Skip to content

Commit 977218e

Browse files
committed
wip: initial version
0 parents  commit 977218e

18 files changed

+4204
-0
lines changed

.eslintrc.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"env": {
3+
"es6": true
4+
},
5+
"ignorePatterns": ["node_modules"],
6+
"extends": [
7+
"eslint:recommended",
8+
"plugin:@typescript-eslint/recommended",
9+
"plugin:import/typescript"
10+
],
11+
"parser": "@typescript-eslint/parser",
12+
"plugins": [
13+
"@typescript-eslint",
14+
"import"
15+
],
16+
"root": true,
17+
"overrides": [
18+
],
19+
"parserOptions": {
20+
"ecmaVersion": "latest",
21+
"sourceType": "module"
22+
},
23+
"rules": {
24+
},
25+
"settings": {
26+
"import/resolver": {
27+
"typescript": true,
28+
"node": true
29+
}
30+
}
31+
}

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.idea
3+
.vscode
4+
.envrc
5+
build

.prettierrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Basejump CLI
2+
Generate new basejump projects from the command line. Also provides a few generator functions for helpful models and components while building.
3+
4+
## Setup
5+
1. Install the package globally: `yarn global add @usebasejump/cli` or `npm install -g @usebasejump/cli`
6+
2. Run `basejump` to see the available commands
7+
8+
Alternatively, you can run `npx @usebasejump/cli` to run the CLI without installing it globally.

jest.config.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
};

package.json

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "@usebasejump/cli",
3+
"version": "0.0.2",
4+
"description": "Generate new basejump projects from the command line",
5+
"main": "build/index.js",
6+
"license": "MIT",
7+
"scripts": {
8+
"build": "tsc",
9+
"test": "jest"
10+
},
11+
"files": [
12+
"build"
13+
],
14+
"bin": {
15+
"basejump": "./build/index.js"
16+
},
17+
"dependencies": {
18+
"chalk": "4.1.2",
19+
"commander": "^10.0.0",
20+
"got": "10.7.0",
21+
"prompts": "^2.4.2",
22+
"tar": "^6.1.13"
23+
},
24+
"devDependencies": {
25+
"@types/jest": "^29.2.5",
26+
"@types/node": "^18.11.18",
27+
"@types/prompts": "^2.4.4",
28+
"@types/tar": "^6.1.4",
29+
"@typescript-eslint/eslint-plugin": "^5.48.1",
30+
"@typescript-eslint/parser": "^5.48.1",
31+
"eslint": "^8.31.0",
32+
"eslint-config-prettier": "^8.6.0",
33+
"eslint-config-standard-with-typescript": "^27.0.1",
34+
"eslint-import-resolver-typescript": "^3.5.3",
35+
"eslint-plugin-import": "^2.27.4",
36+
"eslint-plugin-n": "^15.0.0",
37+
"eslint-plugin-promise": "^6.0.0",
38+
"jest": "^29.3.1",
39+
"prettier": "^2.8.2",
40+
"ts-jest": "^29.0.4",
41+
"typescript": "^4.9.4"
42+
}
43+
}

src/commands/generate.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {logError} from "../utils/logger";
2+
import copyTemplateFile from "../utils/copy-template-file";
3+
import confirmSupabaseRoot from "../utils/confirm-supabase-root";
4+
5+
type TEMPLATE_TYPES = "model";
6+
7+
export default async function(templateType: TEMPLATE_TYPES) {
8+
// make sure we're in a basejump project root
9+
const isSupabaseRoot = confirmSupabaseRoot();
10+
if (!isSupabaseRoot) {
11+
logError("This command must be run from the root of a Basejump project.");
12+
process.exit(1);
13+
}
14+
15+
switch (templateType) {
16+
case "model":
17+
await copyTemplateFile("model.sql", `./supabase/migrations/{{modelName}}.sql`);
18+
break;
19+
default:
20+
logError(`Unknown template type: ${templateType}`);
21+
process.exit(1);
22+
}
23+
}

src/commands/new-project.ts

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {cloneRepo} from "../utils/clone-repo";
2+
import prompts from "prompts";
3+
import {readFileSync, writeFileSync} from "fs";
4+
import generateSeedFile from "../utils/generate-seed-file";
5+
import {logInfo, logSuccess} from "../utils/logger";
6+
7+
/**
8+
* Creates a new Basejump project cloned from a given repo to a given path
9+
* @param projectRepo
10+
* @param projectPath
11+
*/
12+
export default async (
13+
projectRepo: string,
14+
projectPath: string
15+
) => {
16+
// the name of the project is the last part of the projectPath
17+
const pathProjectName = projectPath.split("/").pop();
18+
19+
// ask user for project defaults using prompts library
20+
const {projectName, teamAccounts, personalAccounts, billingProvider} = await prompts([
21+
{
22+
type: "text",
23+
name: "projectName",
24+
message: "What is the name of your project?",
25+
initial: pathProjectName,
26+
},
27+
{
28+
type: "confirm",
29+
name: "teamAccounts",
30+
message: "Do you want to use team accounts?",
31+
initial: true,
32+
},
33+
{
34+
type: "confirm",
35+
name: "personalAccounts",
36+
message: "Do you want to use personal accounts?",
37+
initial: true,
38+
},
39+
{
40+
type: "select",
41+
name: "billingProvider",
42+
message: "Which billing provider do you want to use? Billing is disabled by default and this can be changed later.",
43+
choices: [
44+
{title: "None", value: "none"},
45+
{title: "Stripe", value: "stripe"}
46+
],
47+
initial: 0,
48+
}
49+
]);
50+
51+
logInfo("Setting up your project...");
52+
53+
await cloneRepo(projectRepo, projectPath);
54+
55+
// replace the supabase/seeds.sql file with the correct values
56+
const seedsFile = `${projectPath}/supabase/seed.sql`;
57+
// we know exactly what we want the file to be, so we just overwrite it with the correct values
58+
await writeFileSync(seedsFile, generateSeedFile(teamAccounts, personalAccounts, billingProvider), "utf8");
59+
60+
// replace the supabase project name with the project name
61+
const supabaseConfigFile = `${projectPath}/supabase/config.toml`;
62+
// the project name is stored as project_id = "project-name"
63+
const supabaseConfig = readFileSync(supabaseConfigFile, "utf8");
64+
const newSupabaseConfig = supabaseConfig.replace(/project_id = ".*"/, `project_id = "${projectName}"`);
65+
await writeFileSync(supabaseConfigFile, newSupabaseConfig, "utf8");
66+
logSuccess("Project setup complete!");
67+
logSuccess(`Your project is ready at ${projectPath}`);
68+
}

src/index.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#! /usr/bin/env node
2+
3+
import {Command} from "commander";
4+
5+
const program = new Command();
6+
7+
import newProject from "./commands/new-project";
8+
import generate from "./commands/generate";
9+
10+
/**
11+
* Create a new Basejump project
12+
*/
13+
program
14+
.command("new")
15+
.alias("n")
16+
.argument("<projectpath>", "Where should your new project be created")
17+
.option(
18+
"-r, --repo <projectrepo>",
19+
"Specify which project you want to clone. Must be the TAR download URL of a GitHub repo",
20+
"https://github.com/usebasejump/basejump/archive/main.tar.gz"
21+
)
22+
.description("Generate a new Basejump project")
23+
.action(async (projectPath, options) => {
24+
await newProject(
25+
options.repo,
26+
projectPath
27+
);
28+
});
29+
30+
/**
31+
* Generate a new model off of a template
32+
*/
33+
program
34+
.command("generate")
35+
.alias("g")
36+
.argument("<template>", "The template you want to generate. Ex: 'model'")
37+
.description("Generate a new model off of a template")
38+
.action(async (template) => {
39+
await generate(template);
40+
});
41+
42+
program.parse();

src/templates/model.sql

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
CREATE TABLE IF NOT EXISTS public.{{modelName}}
2+
(
3+
id uuid unique NOT NULL DEFAULT uuid_generate_v4(),
4+
-- If your model is owned by an account, you want to make sure you have an account_id column
5+
-- referencing the account table. Make sure you also set permissions appropriately
6+
account_id uuid not null references accounts(id),
7+
-- timestamps are useful for auditing
8+
-- Basejump has some convenience functions defined below for automatically handling these
9+
updated_at timestamp with time zone,
10+
created_at timestamp with time zone,
11+
PRIMARY KEY (id)
12+
);
13+
14+
15+
-- protect the timestamps by setting created_at and updated_at to be read-only and managed by a trigger
16+
CREATE TRIGGER set_{{modelName}}_timestamp
17+
BEFORE INSERT OR UPDATE ON public.{{modelName}}
18+
FOR EACH ROW
19+
EXECUTE PROCEDURE basejump.trigger_set_timestamps();
20+
21+
22+
-- enable RLS on the table
23+
ALTER TABLE public.{{modelName}} ENABLE ROW LEVEL SECURITY;
24+
25+
26+
-- Because RLS is enabled, this table will NOT be accessible to any users by default
27+
-- You must create a policy for each user that should have access to the table
28+
-- Here are a few example policies that you may find useful when working with Basejump
29+
30+
----------------
31+
-- Authenticated users should be able to read all records regardless of account
32+
----------------
33+
-- create policy "All logged in users can select" on public.{{modelName}}
34+
-- for select
35+
-- to authenticated
36+
-- using (true);
37+
38+
----------------
39+
-- Authenticated AND Anon users should be able to read all records regardless of account
40+
----------------
41+
-- create policy "All authenticated and anonymous users can select" on public.{{modelName}}
42+
-- for select
43+
-- to authenticated, anon
44+
-- using (true);
45+
46+
-------------
47+
-- Users should be able to read records that are owned by an account they belong to
48+
--------------
49+
-- create policy "Account members can select" on public.{{modelName}}
50+
-- for select
51+
-- to authenticated
52+
-- using (
53+
-- (account_id IN ( SELECT basejump.get_accounts_for_current_user()))
54+
-- );
55+
56+
57+
----------------
58+
-- Users should be able to create records that are owned by an account they belong to
59+
----------------
60+
-- create policy "Account members can insert" on public.{{modelName}}
61+
-- for insert
62+
-- to authenticated
63+
-- with check (
64+
-- (account_id IN ( SELECT basejump.get_accounts_for_current_user()))
65+
-- );
66+
67+
---------------
68+
-- Users should be able to update records that are owned by an account they belong to
69+
---------------
70+
-- create policy "Account members can update" on public.{{modelName}}
71+
-- for update
72+
-- to authenticated
73+
-- using (
74+
-- (account_id IN ( SELECT basejump.get_accounts_for_current_user()))
75+
-- );
76+
77+
----------------
78+
-- Users should be able to delete records that are owned by an account they belong to
79+
----------------
80+
-- create policy "Account members can delete" on public.{{modelName}}
81+
-- for delete
82+
-- to authenticated
83+
-- using (
84+
-- (account_id IN ( SELECT basejump.get_accounts_for_current_user()))
85+
-- );
86+
87+
----------------
88+
-- Only account OWNERS should be able to delete records that are owned by an account they belong to
89+
----------------
90+
-- create policy "Account owners can delete" on public.{{modelName}}
91+
-- for delete
92+
-- to authenticated
93+
-- using (
94+
-- (account_id IN ( SELECT basejump.get_accounts_for_current_user("owner")))
95+
-- );
96+

src/templates/seed.sql

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
insert into basejump.config (enable_personal_accounts,
2+
enable_team_accounts,
3+
enable_account_billing,
4+
billing_provider
5+
) values ({{personalAccounts}}, {{teamAccounts}}, FALSE, '{{billingProvider}}');

src/utils/clone-repo.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { join } from 'path'
2+
import {tmpdir} from "os";
3+
import { Stream } from 'stream'
4+
import { promisify } from 'util'
5+
import tar from "tar";
6+
import {createWriteStream, promises as fs} from "fs";
7+
import got from "got"
8+
9+
const pipeline = promisify(Stream.pipeline)
10+
11+
/**
12+
* Downloads a given repo URL to a temporary file
13+
* @param repo
14+
*/
15+
async function downloadRepo(repo: string) {
16+
const tempFile = join(tmpdir(), `basejump-clone-${Date.now()}`)
17+
await pipeline(got.stream(repo), createWriteStream(tempFile))
18+
return tempFile
19+
}
20+
21+
/**
22+
* Clones a repo to a given path
23+
* @param repo
24+
* @param projectPath
25+
*/
26+
export async function cloneRepo(repo: string, projectPath: string) {
27+
const tempFile = await downloadRepo(repo);
28+
await fs.mkdir(projectPath, {recursive: true})
29+
await tar.x({
30+
file: tempFile,
31+
cwd: projectPath,
32+
strip: 1
33+
})
34+
35+
await fs.unlink(tempFile)
36+
}

src/utils/confirm-supabase-root.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {existsSync} from "fs";
2+
3+
export default function(): boolean {
4+
// make sure we're in a basejump project root
5+
const supabaseConfigExists = existsSync("./supabase/config.toml");
6+
// make sure supabase migrations folder exists
7+
const supabaseMigrationsExists = existsSync("./supabase/migrations");
8+
return supabaseConfigExists && supabaseMigrationsExists;
9+
}

0 commit comments

Comments
 (0)