@protobase/cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/entry-code.js +4 -1
- package/dist/dev/entry.js +2 -0
- package/dist/dev/project.js +8 -1
- package/dist/files/cleanup-command.js +11 -0
- package/dist/files/load-project.js +47 -0
- package/dist/files/register.js +19 -0
- package/dist/infer/compat.js +1 -1
- package/dist/main.js +4 -0
- package/dist/organizations/commands.js +92 -0
- package/dist/organizations/register.js +70 -0
- package/dist/package.js +1 -1
- package/dist/project/functions-folder.js +27 -0
- package/dist/serve/app.js +2 -0
- package/dist/serve/cleanup-timer.js +22 -0
- package/dist/serve/env.js +4 -1
- package/dist/serve/host-module-ids.js +3 -0
- package/dist/serve/host-modules.js +6 -0
- package/dist/serve/lifecycle.js +9 -1
- package/dist/users/register.js +11 -2
- package/package.json +6 -6
package/dist/build/entry-code.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { moduleFile } from "../module-file.js";
|
|
2
|
+
import { functionFiles } from "../project/functions-folder.js";
|
|
2
3
|
import { existsSync, readdirSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
//#region src/build/entry-code.ts
|
|
@@ -16,8 +17,10 @@ const bundleEntryCode = (projectDir, extensions = []) => {
|
|
|
16
17
|
const dirs = uiDirs(configDir);
|
|
17
18
|
lines.push(`import { conventionConfig } from ${JSON.stringify(conventionModule)}`, `import * as index from ${JSON.stringify(index)}`, ...dirs.map((dir, i) => `import * as ui${i} from ${JSON.stringify(path.join(configDir, dir, "ui.ts"))}`), `const convention = conventionConfig(index, [${dirs.map((dir, i) => `[${JSON.stringify(dir)}, ui${i}]`).join(", ")}])`);
|
|
18
19
|
} else lines.push("const convention = undefined");
|
|
20
|
+
const functions = functionFiles(projectDir);
|
|
21
|
+
lines.push(...functions.map(({ file }, i) => `import * as function${i} from ${JSON.stringify(file)}`), `const functions = ${functions.length === 0 ? "undefined" : `{ ${functions.map(({ name }, i) => `${JSON.stringify(name)}: function${i}.default`).join(", ")} }`}`);
|
|
19
22
|
const configs = extensions.flatMap((extension) => extension.config ? [extension.config] : []);
|
|
20
|
-
lines.push(`import { mergeConfig } from '@protobase/server'`, ...configs.map((file, i) => `import * as extension${i} from ${JSON.stringify(file)}`), "const named = (config, file) => ({ ...config, name: config.name ?? file })", `export default mergeConfig(${[...configs.map((file, i) => `named(extension${i}.default ?? {}, ${JSON.stringify(file)})`), `named({ ...project, config: project.config ?? convention }, ${JSON.stringify(hasCustom ? "protobase.config.ts" : "config/index.ts")})`].join(", ")})`);
|
|
23
|
+
lines.push(`import { mergeConfig } from '@protobase/server'`, ...configs.map((file, i) => `import * as extension${i} from ${JSON.stringify(file)}`), "const named = (config, file) => ({ ...config, name: config.name ?? file })", `export default mergeConfig(${[...configs.map((file, i) => `named(extension${i}.default ?? {}, ${JSON.stringify(file)})`), `named({ ...project, config: project.config ?? convention, functions: project.functions ?? functions }, ${JSON.stringify(hasCustom ? "protobase.config.ts" : "config/index.ts")})`].join(", ")})`);
|
|
21
24
|
return `${lines.join("\n")}\n`;
|
|
22
25
|
};
|
|
23
26
|
//#endregion
|
package/dist/dev/entry.js
CHANGED
|
@@ -20,6 +20,8 @@ const admin = createAdmin({
|
|
|
20
20
|
db,
|
|
21
21
|
authenticate: project.authenticate,
|
|
22
22
|
...project.auth && { auth: project.auth },
|
|
23
|
+
...project.functions && { functions: project.functions },
|
|
24
|
+
...project.files && { files: project.files },
|
|
23
25
|
options: {
|
|
24
26
|
onUnhandledError: (error) => console.error(error),
|
|
25
27
|
...project.options
|
package/dist/dev/project.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { functionFiles } from "../project/functions-folder.js";
|
|
1
2
|
import { conventionConfig } from "../project/convention.js";
|
|
2
3
|
import { sharedDb } from "./create-db.js";
|
|
3
4
|
import { existsSync } from "node:fs";
|
|
@@ -22,6 +23,11 @@ const conventionExports = async (projectDir) => {
|
|
|
22
23
|
}
|
|
23
24
|
return conventionConfig(indexExports, uiModules);
|
|
24
25
|
};
|
|
26
|
+
const conventionFunctions = async (projectDir) => {
|
|
27
|
+
const files = functionFiles(projectDir);
|
|
28
|
+
if (files.length === 0) return void 0;
|
|
29
|
+
return Object.fromEntries(await Promise.all(files.map(async ({ name, file }) => [name, (await load(file)).default])));
|
|
30
|
+
};
|
|
25
31
|
const named = (config, file) => ({
|
|
26
32
|
...config,
|
|
27
33
|
name: config.name ?? file
|
|
@@ -33,7 +39,8 @@ const loadProject = async (projectDir, extensions = []) => {
|
|
|
33
39
|
for (const extension of extensions) if (extension.config) extended.push(named((await load(extension.config)).default ?? {}, extension.config));
|
|
34
40
|
const own = named({
|
|
35
41
|
...custom,
|
|
36
|
-
config: custom.config ?? await conventionExports(projectDir)
|
|
42
|
+
config: custom.config ?? await conventionExports(projectDir),
|
|
43
|
+
functions: custom.functions ?? await conventionFunctions(projectDir)
|
|
37
44
|
}, existsSync(file) ? "protobase.config.ts" : "config/index.ts");
|
|
38
45
|
const project = mergeConfig(...extended, own);
|
|
39
46
|
return {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/files/cleanup-command.ts
|
|
2
|
+
const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
3
|
+
/** Runs the scheduled file deletes that are due and says what happened to each file. */
|
|
4
|
+
const cleanupCommand = async (run, out) => {
|
|
5
|
+
const { deleted, kept } = await run();
|
|
6
|
+
if (deleted.length === 0 && kept.length === 0) return out("No file deletes were due\n");
|
|
7
|
+
if (deleted.length > 0) out(`Deleted ${plural(deleted.length, "file")}:\n${deleted.map((uri) => ` ${uri}\n`).join("")}`);
|
|
8
|
+
if (kept.length > 0) out(`Kept ${plural(kept.length, "file")} a row references again:\n${kept.map((uri) => ` ${uri}\n`).join("")}`);
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { cleanupCommand };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { loadNearestEnvFile } from "../dev/env.js";
|
|
2
|
+
import { importProjectFile } from "../project/import-project-file.js";
|
|
3
|
+
import { readManifest } from "../bundle/manifest.js";
|
|
4
|
+
import { createPgDb } from "../project/pg-db.js";
|
|
5
|
+
import { loadBundle } from "../serve/load-bundle.js";
|
|
6
|
+
import { existsSync, statSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { configExports } from "@protobase/server";
|
|
9
|
+
import pg from "pg";
|
|
10
|
+
//#region src/files/load-project.ts
|
|
11
|
+
const bundleProject = async (bundle) => {
|
|
12
|
+
if (!statSync(bundle).isDirectory()) return loadBundle(bundle);
|
|
13
|
+
const manifest = await readManifest(bundle);
|
|
14
|
+
return loadBundle(path.join(bundle, manifest.server));
|
|
15
|
+
};
|
|
16
|
+
const sourceProject = async (dir) => {
|
|
17
|
+
const file = path.join(dir, "protobase.config.ts");
|
|
18
|
+
const custom = existsSync(file) ? (await importProjectFile(file)).default ?? {} : {};
|
|
19
|
+
if (custom.config) return custom;
|
|
20
|
+
const index = path.join(dir, "config", "index.ts");
|
|
21
|
+
if (!existsSync(index)) throw new Error(`No config/index.ts or protobase.config.ts in ${dir}; run this inside a project, or name a bundle`);
|
|
22
|
+
return {
|
|
23
|
+
...custom,
|
|
24
|
+
config: await importProjectFile(index)
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* What a command over a project's files needs: its resources, its files options and a database, from a bundle or from
|
|
29
|
+
* the project in `dir`. `close` ends the pool it opened; a `db` the project exports stays the project's.
|
|
30
|
+
*/
|
|
31
|
+
const loadFilesProject = async (bundle, dir) => {
|
|
32
|
+
loadNearestEnvFile(dir);
|
|
33
|
+
const project = bundle ? await bundleProject(bundle) : await sourceProject(dir);
|
|
34
|
+
const url = process.env.DATABASE_URL;
|
|
35
|
+
if (!project.db && !url) throw new Error("No database: set DATABASE_URL or export `db` from protobase.config.ts");
|
|
36
|
+
const ownDb = project.db ? void 0 : createPgDb(pg, url);
|
|
37
|
+
return {
|
|
38
|
+
resources: configExports(project.config ?? {}).resources,
|
|
39
|
+
db: project.db ?? ownDb,
|
|
40
|
+
...project.files && { files: project.files },
|
|
41
|
+
close: async () => {
|
|
42
|
+
await ownDb?.destroy();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { loadFilesProject };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cleanupCommand } from "./cleanup-command.js";
|
|
2
|
+
import { loadFilesProject } from "./load-project.js";
|
|
3
|
+
import { createFileCleanup } from "@protobase/server";
|
|
4
|
+
//#region src/files/register.ts
|
|
5
|
+
const out = (text) => process.stdout.write(text);
|
|
6
|
+
const registerFiles = (program) => {
|
|
7
|
+
program.command("files").description("Manage the stored files of the project in the current directory").command("cleanup").description("Delete the files whose scheduled delete is due and that no row references, such as replaced files and unused uploads").argument("[bundle]", "a bundle from `protobase build` (its folder or config module); default: the project in the current directory").action(async (bundle) => {
|
|
8
|
+
const project = await loadFilesProject(bundle, process.cwd());
|
|
9
|
+
try {
|
|
10
|
+
const cleanup = createFileCleanup(project);
|
|
11
|
+
if (cleanup) await cleanupCommand(cleanup.run, out);
|
|
12
|
+
else out("The project has no file fields\n");
|
|
13
|
+
} finally {
|
|
14
|
+
await project.close();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { registerFiles };
|
package/dist/infer/compat.js
CHANGED
|
@@ -8,7 +8,7 @@ const relationTargets = [
|
|
|
8
8
|
const typesCompatible = (declared, base) => {
|
|
9
9
|
if (declared === base) return true;
|
|
10
10
|
if (declared === "relation") return relationTargets.includes(base);
|
|
11
|
-
if (declared === "currency" || declared === "country") return base === "text";
|
|
11
|
+
if (declared === "currency" || declared === "country" || declared === "file") return base === "text";
|
|
12
12
|
if (declared === "enum") return base === "text";
|
|
13
13
|
if (declared === "text") return base === "enum";
|
|
14
14
|
return false;
|
package/dist/main.js
CHANGED
|
@@ -11,6 +11,8 @@ import { runDoctor } from "./doctor/run.js";
|
|
|
11
11
|
import { runScaffold } from "./scaffold/run.js";
|
|
12
12
|
import { serveBundle } from "./serve/serve-bundle.js";
|
|
13
13
|
import { serveBundleDir } from "./serve/serve-dir.js";
|
|
14
|
+
import { registerOrganizations } from "./organizations/register.js";
|
|
15
|
+
import { registerFiles } from "./files/register.js";
|
|
14
16
|
import { registerToken, registerUsers } from "./users/register.js";
|
|
15
17
|
import { statSync } from "node:fs";
|
|
16
18
|
import path from "node:path";
|
|
@@ -84,8 +86,10 @@ program.command("serve").description("Serve a bundle written by `protobase build
|
|
|
84
86
|
else await serveBundle(bundle, process.env, out);
|
|
85
87
|
});
|
|
86
88
|
registerUsers(program);
|
|
89
|
+
registerOrganizations(program);
|
|
87
90
|
registerToken(program);
|
|
88
91
|
registerAuth(program);
|
|
92
|
+
registerFiles(program);
|
|
89
93
|
const run = (argv) => program.parseAsync(argv).catch((error) => {
|
|
90
94
|
process.stderr.write(`error: ${error.message}\n`);
|
|
91
95
|
process.exitCode = 1;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//#region src/organizations/commands.ts
|
|
2
|
+
/** A slug from a name: lowercase ASCII words joined by dashes; diacritics are dropped, so Müller becomes muller. */
|
|
3
|
+
const slugOf = (name) => name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
4
|
+
/** `a,b` as a list of roles; an empty text is none. */
|
|
5
|
+
const parseRoleList = (text) => text === void 0 ? void 0 : text.split(",").map((name) => name.trim()).filter(Boolean);
|
|
6
|
+
const listOrganizationsCommand = async (api, out) => {
|
|
7
|
+
const organizations = await api.listOrganizations();
|
|
8
|
+
if (organizations.length === 0) {
|
|
9
|
+
out("No organizations yet. Create one with: protobase organizations create \"Acme\" --owner you@example.com\n");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
for (const organization of organizations) {
|
|
13
|
+
out(`${organization.slug} ${organization.name} (${organization.id})\n`);
|
|
14
|
+
for (const member of organization.members) out(` ${member.email} ${member.role}${member.appRoles.length > 0 ? ` ${member.appRoles.join(",")}` : ""}\n`);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Creates an organization owned by `owner`. With `--members all`, for an app that ran as one organization: superusers
|
|
19
|
+
* join as owners with every app role and stay superusers; everyone else joins as a member, and their global roles move
|
|
20
|
+
* onto the membership, since a global role would let them into every organization once there is a second.
|
|
21
|
+
*/
|
|
22
|
+
const createOrganizationCommand = async (api, options, out) => {
|
|
23
|
+
if (options.members !== void 0 && options.members !== "all") throw new Error(`--members takes "all", not "${options.members}"`);
|
|
24
|
+
const slug = options.slug ?? slugOf(options.name);
|
|
25
|
+
const { organization } = await api.createOrganization({
|
|
26
|
+
name: options.name,
|
|
27
|
+
slug,
|
|
28
|
+
owner: options.owner,
|
|
29
|
+
...options.id && { id: options.id }
|
|
30
|
+
});
|
|
31
|
+
out(`Created organization ${organization.slug} (${organization.id}), owned by ${options.owner}\n`);
|
|
32
|
+
if (options.members !== "all") return;
|
|
33
|
+
const holdable = api.membershipRoles();
|
|
34
|
+
for (const user of await api.listUsers()) {
|
|
35
|
+
if (user.email.toLowerCase() === options.owner.toLowerCase()) continue;
|
|
36
|
+
const global = user.role.split(",").map((name) => name.trim()).filter(Boolean);
|
|
37
|
+
if (global.includes("admin")) {
|
|
38
|
+
await api.addMember({
|
|
39
|
+
organization: organization.id,
|
|
40
|
+
email: user.email,
|
|
41
|
+
role: "owner",
|
|
42
|
+
appRoles: holdable
|
|
43
|
+
});
|
|
44
|
+
out(` ${user.email}: owner, with every app role\n`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const appRoles = global.filter((name) => holdable.includes(name));
|
|
48
|
+
await api.addMember({
|
|
49
|
+
organization: organization.id,
|
|
50
|
+
email: user.email,
|
|
51
|
+
role: "member",
|
|
52
|
+
appRoles
|
|
53
|
+
});
|
|
54
|
+
await api.setUserRole({
|
|
55
|
+
email: user.email,
|
|
56
|
+
role: "user"
|
|
57
|
+
});
|
|
58
|
+
out(` ${user.email}: member${appRoles.length > 0 ? `, with ${appRoles.join(",")}` : ""}\n`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const addMemberCommand = async (api, options, out) => {
|
|
62
|
+
const appRoles = parseRoleList(options.appRoles);
|
|
63
|
+
await api.addMember({
|
|
64
|
+
organization: options.organization,
|
|
65
|
+
email: options.email,
|
|
66
|
+
...options.orgRole && { role: options.orgRole },
|
|
67
|
+
...appRoles && { appRoles }
|
|
68
|
+
});
|
|
69
|
+
out(`Added ${options.email} to ${options.organization}\n`);
|
|
70
|
+
};
|
|
71
|
+
const setMemberRolesCommand = async (api, options, out) => {
|
|
72
|
+
const appRoles = parseRoleList(options.appRoles);
|
|
73
|
+
if (options.orgRole === void 0 && appRoles === void 0) throw new Error("Pass --org-role, --app-roles or both");
|
|
74
|
+
await api.setMemberRoles({
|
|
75
|
+
organization: options.organization,
|
|
76
|
+
email: options.email,
|
|
77
|
+
...options.orgRole && { role: options.orgRole },
|
|
78
|
+
...appRoles && { appRoles }
|
|
79
|
+
});
|
|
80
|
+
out(`Changed the roles of ${options.email} in ${options.organization}\n`);
|
|
81
|
+
};
|
|
82
|
+
const removeMemberCommand = async (api, options, out) => {
|
|
83
|
+
await api.removeMember(options);
|
|
84
|
+
out(`Removed ${options.email} from ${options.organization}\n`);
|
|
85
|
+
};
|
|
86
|
+
const addAppRoleCommand = async (api, options, out) => {
|
|
87
|
+
if (options.to !== "owners") throw new Error(`--to takes "owners", not "${options.to}"`);
|
|
88
|
+
const changed = await api.addAppRoleToOwners(options.role);
|
|
89
|
+
out(`Gave ${options.role} to ${changed} owner${changed === 1 ? "" : "s"}\n`);
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { addAppRoleCommand, addMemberCommand, createOrganizationCommand, listOrganizationsCommand, parseRoleList, removeMemberCommand, setMemberRolesCommand, slugOf };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { loadProjectAuth } from "../users/load-auth.js";
|
|
2
|
+
import { addAppRoleCommand, addMemberCommand, createOrganizationCommand, listOrganizationsCommand, removeMemberCommand, setMemberRolesCommand } from "./commands.js";
|
|
3
|
+
import { addAppRoleToOwners, addMember, createOrganization, listOrganizations, listUsers, removeMember, setMemberRoles, setUserRole } from "@protobase/server";
|
|
4
|
+
//#region src/organizations/register.ts
|
|
5
|
+
const out = (text) => process.stdout.write(text);
|
|
6
|
+
const organizationsApi = (auth) => ({
|
|
7
|
+
listOrganizations: () => listOrganizations(auth),
|
|
8
|
+
createOrganization: (input) => createOrganization(auth, input),
|
|
9
|
+
addMember: (input) => addMember(auth, input),
|
|
10
|
+
setMemberRoles: (input) => setMemberRoles(auth, input),
|
|
11
|
+
removeMember: (input) => removeMember(auth, input),
|
|
12
|
+
addAppRoleToOwners: (appRole) => addAppRoleToOwners(auth, appRole),
|
|
13
|
+
listUsers: () => listUsers(auth),
|
|
14
|
+
setUserRole: (input) => setUserRole(auth, input),
|
|
15
|
+
membershipRoles: () => auth.roleDefinitions.names.filter((name) => name !== "admin" && name !== "user")
|
|
16
|
+
});
|
|
17
|
+
const projectApi = async () => organizationsApi(await loadProjectAuth(process.cwd()));
|
|
18
|
+
const finish = (code) => process.exit(code);
|
|
19
|
+
const memberOptions = (command) => command.argument("<organization>", "its slug or id").argument("<email>").option("--org-role <role>", "owner, admin or member: who manages the organization").option("--app-roles <roles>", "app roles in this organization, comma separated (see `users roles`)");
|
|
20
|
+
const registerOrganizations = (program) => {
|
|
21
|
+
const organizations = program.command("organizations").description("Manage the organizations of the project in the current directory (with `organizations` in createAuth)");
|
|
22
|
+
organizations.command("list").description("List organizations with their members and roles").action(async () => {
|
|
23
|
+
await listOrganizationsCommand(await projectApi(), out);
|
|
24
|
+
finish(0);
|
|
25
|
+
});
|
|
26
|
+
organizations.command("create").description("Create an organization; its owner holds every app role").argument("<name>").requiredOption("--owner <email>", "the user who owns it").option("--slug <slug>", "default: from the name").option("--id <id>", "keep an existing tenant value, for an app that ran as one organization").option("--members <all>", "\"all\": every other user joins; their global roles move here, superusers become owners").action(async (name, opts) => {
|
|
27
|
+
await createOrganizationCommand(await projectApi(), {
|
|
28
|
+
name,
|
|
29
|
+
owner: opts.owner,
|
|
30
|
+
slug: opts.slug,
|
|
31
|
+
id: opts.id,
|
|
32
|
+
members: opts.members
|
|
33
|
+
}, out);
|
|
34
|
+
finish(0);
|
|
35
|
+
});
|
|
36
|
+
memberOptions(organizations.command("add-member").description("Add a user to an organization (organization role member unless given)")).action(async (organization, email, opts) => {
|
|
37
|
+
await addMemberCommand(await projectApi(), {
|
|
38
|
+
organization,
|
|
39
|
+
email,
|
|
40
|
+
orgRole: opts.orgRole,
|
|
41
|
+
appRoles: opts.appRoles
|
|
42
|
+
}, out);
|
|
43
|
+
finish(0);
|
|
44
|
+
});
|
|
45
|
+
memberOptions(organizations.command("set-roles").description("Change a member's organization role, app roles, or both")).action(async (organization, email, opts) => {
|
|
46
|
+
await setMemberRolesCommand(await projectApi(), {
|
|
47
|
+
organization,
|
|
48
|
+
email,
|
|
49
|
+
orgRole: opts.orgRole,
|
|
50
|
+
appRoles: opts.appRoles
|
|
51
|
+
}, out);
|
|
52
|
+
finish(0);
|
|
53
|
+
});
|
|
54
|
+
organizations.command("remove-member").description("Remove a member (refuses the last owner)").argument("<organization>", "its slug or id").argument("<email>").action(async (organization, email) => {
|
|
55
|
+
await removeMemberCommand(await projectApi(), {
|
|
56
|
+
organization,
|
|
57
|
+
email
|
|
58
|
+
}, out);
|
|
59
|
+
finish(0);
|
|
60
|
+
});
|
|
61
|
+
organizations.command("add-app-role").description("Give an app role to every owner of every organization, for a role added after they were made").argument("<role>").requiredOption("--to <owners>", "\"owners\"").action(async (role, opts) => {
|
|
62
|
+
await addAppRoleCommand(await projectApi(), {
|
|
63
|
+
role,
|
|
64
|
+
to: opts.to
|
|
65
|
+
}, out);
|
|
66
|
+
finish(0);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
export { organizationsApi, registerOrganizations };
|
package/dist/package.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/project/functions-folder.ts
|
|
4
|
+
const skipped = (entry) => entry.startsWith("_") || entry.startsWith(".") || /\.(test|spec|d)\.ts$/.test(entry);
|
|
5
|
+
const functionFiles = (projectDir) => {
|
|
6
|
+
const dir = path.join(projectDir, "functions");
|
|
7
|
+
if (!existsSync(dir)) return [];
|
|
8
|
+
const found = /* @__PURE__ */ new Map();
|
|
9
|
+
const add = (name, file) => {
|
|
10
|
+
if (found.has(name)) throw new Error(`The function "${name}" is both ${found.get(name)} and ${file}; keep one of them`);
|
|
11
|
+
found.set(name, file);
|
|
12
|
+
};
|
|
13
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
+
if (skipped(entry.name)) continue;
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
const index = path.join(dir, entry.name, "index.ts");
|
|
17
|
+
if (!existsSync(index)) throw new Error(`functions/${entry.name} has no index.ts; a folder of shared code is named with a leading _, such as functions/_${entry.name}`);
|
|
18
|
+
add(entry.name, index);
|
|
19
|
+
} else if (entry.name.endsWith(".ts")) add(entry.name.slice(0, -3), path.join(dir, entry.name));
|
|
20
|
+
}
|
|
21
|
+
return [...found].map(([name, file]) => ({
|
|
22
|
+
name,
|
|
23
|
+
file
|
|
24
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { functionFiles };
|
package/dist/serve/app.js
CHANGED
|
@@ -14,6 +14,8 @@ const createServeApp = ({ project, db, requestLog, site }) => {
|
|
|
14
14
|
db,
|
|
15
15
|
authenticate: project.authenticate,
|
|
16
16
|
...project.auth && { auth: project.auth },
|
|
17
|
+
...project.functions && { functions: project.functions },
|
|
18
|
+
...project.files && { files: project.files },
|
|
17
19
|
options: {
|
|
18
20
|
onUnhandledError: (error) => console.error(error),
|
|
19
21
|
...project.options
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/serve/cleanup-timer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Runs `run` every `minutes`, never two at once; a failed run is reported and the next one still comes. `stop` ends the
|
|
4
|
+
* timer and waits for a run in progress.
|
|
5
|
+
*/
|
|
6
|
+
const startCleanupTimer = (run, minutes, report) => {
|
|
7
|
+
let running;
|
|
8
|
+
const tick = () => {
|
|
9
|
+
if (running) return;
|
|
10
|
+
running = run().then(() => void 0, report).finally(() => {
|
|
11
|
+
running = void 0;
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
const timer = setInterval(tick, minutes * 6e4);
|
|
15
|
+
timer.unref?.();
|
|
16
|
+
return { stop: async () => {
|
|
17
|
+
clearInterval(timer);
|
|
18
|
+
await running;
|
|
19
|
+
} };
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { startCleanupTimer };
|
package/dist/serve/env.js
CHANGED
|
@@ -3,10 +3,13 @@ const defaultPort = 8787;
|
|
|
3
3
|
const readServeEnv = (env) => {
|
|
4
4
|
const port = env.PORT ? Number(env.PORT) : defaultPort;
|
|
5
5
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`PORT must be a port number, not "${env.PORT}"`);
|
|
6
|
+
const minutes = env.PROTOBASE_FILES_CLEANUP_MINUTES ? Number(env.PROTOBASE_FILES_CLEANUP_MINUTES) : 60;
|
|
7
|
+
if (!Number.isInteger(minutes) || minutes < 0) throw new Error(`PROTOBASE_FILES_CLEANUP_MINUTES must be a whole number of minutes, not "${env.PROTOBASE_FILES_CLEANUP_MINUTES}"`);
|
|
6
8
|
return {
|
|
7
9
|
port,
|
|
8
10
|
databaseUrl: env.DATABASE_URL || void 0,
|
|
9
|
-
requestLog: Boolean(env.REQUEST_LOG)
|
|
11
|
+
requestLog: Boolean(env.REQUEST_LOG),
|
|
12
|
+
filesCleanupMinutes: minutes
|
|
10
13
|
};
|
|
11
14
|
};
|
|
12
15
|
//#endregion
|
|
@@ -8,6 +8,9 @@ import * as hono from "hono";
|
|
|
8
8
|
import * as apiReference from "@scalar/hono-api-reference";
|
|
9
9
|
import * as aipParsers from "aip-parsers";
|
|
10
10
|
import * as betterAuth from "better-auth";
|
|
11
|
+
import * as honoCors from "hono/cors";
|
|
12
|
+
import * as honoHttpException from "hono/http-exception";
|
|
13
|
+
import * as honoValidator from "hono/validator";
|
|
11
14
|
import * as jose from "jose";
|
|
12
15
|
import * as kyselyPostgres from "kysely/helpers/postgres";
|
|
13
16
|
import * as zod from "zod";
|
|
@@ -26,6 +29,9 @@ const hostModules = {
|
|
|
26
29
|
postgres: postgres$1,
|
|
27
30
|
"better-auth": betterAuth,
|
|
28
31
|
hono,
|
|
32
|
+
"hono/cors": honoCors,
|
|
33
|
+
"hono/http-exception": honoHttpException,
|
|
34
|
+
"hono/validator": honoValidator,
|
|
29
35
|
jose,
|
|
30
36
|
zod,
|
|
31
37
|
"aip-parsers": aipParsers,
|
package/dist/serve/lifecycle.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createPgDb } from "../project/pg-db.js";
|
|
2
2
|
import { createServeApp } from "./app.js";
|
|
3
|
-
import {
|
|
3
|
+
import { startCleanupTimer } from "./cleanup-timer.js";
|
|
4
|
+
import { checkAuthSchema, configExports, createFileCleanup } from "@protobase/server";
|
|
4
5
|
import { createAdaptorServer } from "@hono/node-server";
|
|
5
6
|
import pg from "pg";
|
|
6
7
|
//#region src/serve/lifecycle.ts
|
|
@@ -28,8 +29,15 @@ const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url
|
|
|
28
29
|
await ownDb?.destroy();
|
|
29
30
|
throw error;
|
|
30
31
|
});
|
|
32
|
+
const cleanup = createFileCleanup({
|
|
33
|
+
resources: configExports(project.config).resources,
|
|
34
|
+
db,
|
|
35
|
+
...project.files && { files: project.files }
|
|
36
|
+
});
|
|
37
|
+
const timer = cleanup && env.filesCleanupMinutes > 0 ? startCleanupTimer(cleanup.run, env.filesCleanupMinutes, (error) => console.error(error)) : void 0;
|
|
31
38
|
const close = async () => {
|
|
32
39
|
await closeServer(server);
|
|
40
|
+
await timer?.stop();
|
|
33
41
|
await ownDb?.destroy();
|
|
34
42
|
};
|
|
35
43
|
return {
|
package/dist/users/register.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { loadProjectAuth } from "./load-auth.js";
|
|
2
|
+
import { addMemberCommand } from "../organizations/commands.js";
|
|
3
|
+
import { organizationsApi } from "../organizations/register.js";
|
|
2
4
|
import { readHidden, rejectPasswordArgument } from "./password.js";
|
|
3
5
|
import { createUserCommand, listRolesCommand, listUsersCommand } from "./commands.js";
|
|
4
6
|
import { deleteUserCommand, setDisabledCommand, setRoleCommand } from "./manage.js";
|
|
@@ -34,9 +36,10 @@ const ask = async (question) => {
|
|
|
34
36
|
const finish = (code) => process.exit(code);
|
|
35
37
|
const registerUsers = (program) => {
|
|
36
38
|
const users = program.command("users").description("Manage the admin users of the project in the current directory");
|
|
37
|
-
users.command("create").description("Create a user; the first user is always an admin").argument("<email>").option("--name <name>", "display name").option("--role <role>", "role for users after the first (default: the project's default role; see `users roles`)").option("--password-stdin", "read the password from stdin", false).option("--generate-password", "generate a strong password and print it once", false).option("--github-id <id>", "link the user to this GitHub user id, so signing in with GitHub finds them whatever their address").addOption(new Option("--password <value>").hideHelp()).action(async (email, opts) => {
|
|
39
|
+
users.command("create").description("Create a user; the first user is always an admin").argument("<email>").option("--name <name>", "display name").option("--role <role>", "role for users after the first (default: the project's default role; see `users roles`)").option("--password-stdin", "read the password from stdin", false).option("--generate-password", "generate a strong password and print it once", false).option("--github-id <id>", "link the user to this GitHub user id, so signing in with GitHub finds them whatever their address").option("--organization <organization>", "with organizations: add the user to this one (its slug or id)").option("--org-role <role>", "with --organization: owner, admin or member (default member)").option("--app-roles <roles>", "with --organization: app roles there, comma separated").addOption(new Option("--password <value>").hideHelp()).action(async (email, opts) => {
|
|
38
40
|
rejectPasswordArgument(opts.password);
|
|
39
|
-
|
|
41
|
+
const auth = await loadProjectAuth(process.cwd());
|
|
42
|
+
await createUserCommand(usersApi(auth), {
|
|
40
43
|
email,
|
|
41
44
|
name: opts.name,
|
|
42
45
|
role: opts.role,
|
|
@@ -47,6 +50,12 @@ const registerUsers = (program) => {
|
|
|
47
50
|
stdin: process.stdin,
|
|
48
51
|
prompt: readHidden
|
|
49
52
|
}, out);
|
|
53
|
+
if (opts.organization) await addMemberCommand(organizationsApi(auth), {
|
|
54
|
+
organization: opts.organization,
|
|
55
|
+
email,
|
|
56
|
+
orgRole: opts.orgRole,
|
|
57
|
+
appRoles: opts.appRoles
|
|
58
|
+
}, out);
|
|
50
59
|
finish(0);
|
|
51
60
|
});
|
|
52
61
|
users.command("delete").description("Delete a user (refuses the last admin)").argument("<email>").option("--yes", "skip the confirmation", false).action(async (email, opts) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@hono/node-server": "^2.1.3",
|
|
36
36
|
"@hono/vite-dev-server": "^0.26.1",
|
|
37
|
-
"@protobase/layout": "0.
|
|
38
|
-
"@protobase/query": "0.
|
|
39
|
-
"@protobase/schema": "0.
|
|
40
|
-
"@protobase/server": "0.
|
|
41
|
-
"@protobase/ui": "0.
|
|
37
|
+
"@protobase/layout": "0.8.0",
|
|
38
|
+
"@protobase/query": "0.8.0",
|
|
39
|
+
"@protobase/schema": "0.8.0",
|
|
40
|
+
"@protobase/server": "0.8.0",
|
|
41
|
+
"@protobase/ui": "0.8.0",
|
|
42
42
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
43
43
|
"@tailwindcss/vite": "^4.3.3",
|
|
44
44
|
"@vitejs/plugin-react": "^6.1.2",
|