@protobase/cli 0.8.1 → 0.8.2

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.
@@ -0,0 +1,25 @@
1
+ import { checkJobsSchema, startJobs } from "@protobase/server";
2
+ //#region src/dev/dev-jobs.ts
3
+ const store = globalThis;
4
+ /**
5
+ * Runs the project's jobs in the dev server: started on the first load, once the jobs schema checks out, and given the
6
+ * reloaded definitions on every load after it, so an edited job runs its new code from the next job on.
7
+ */
8
+ const devJobs = async (db, definitions) => {
9
+ if (store.__protobaseJobs) return store.__protobaseJobs.update(definitions);
10
+ if (definitions.length === 0) return;
11
+ await checkJobsSchema(db);
12
+ store.__protobaseJobs = await startJobs({
13
+ db,
14
+ jobs: definitions,
15
+ onError: (error) => console.error(error)
16
+ });
17
+ };
18
+ /** Lets running jobs finish and stops them, when the dev server closes. */
19
+ const stopDevJobs = async () => {
20
+ const runtime = store.__protobaseJobs;
21
+ delete store.__protobaseJobs;
22
+ await runtime?.stop();
23
+ };
24
+ //#endregion
25
+ export { devJobs, stopDevJobs };
package/dist/dev/entry.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { devJobs } from "./dev-jobs.js";
1
2
  import { loadProject, projectDb } from "./project.js";
2
- import { checkAuthSchema, checkChanges, configExports, createAdmin, hasUsers } from "@protobase/server";
3
+ import { checkAuthSchema, checkChanges, configExports, createAdmin, hasUsers, jobDefinitions } from "@protobase/server";
3
4
  import { Hono } from "hono";
4
5
  //#region src/dev/entry.ts
5
6
  const projectDir = process.env.PROTOBASE_PROJECT;
@@ -13,6 +14,7 @@ if (project.auth && !store.__protobaseAuthSchemaChecked) {
13
14
  store.__protobaseAuthSchemaChecked = true;
14
15
  }
15
16
  await checkChanges(db, resources.map((resource) => resource.toModel()), project.events);
17
+ await devJobs(db, jobDefinitions(project.jobs ?? {}));
16
18
  const admin = createAdmin({
17
19
  resources,
18
20
  views,
@@ -24,6 +26,7 @@ const admin = createAdmin({
24
26
  ...project.functions && { functions: project.functions },
25
27
  ...project.files && { files: project.files },
26
28
  ...project.events && { events: project.events },
29
+ jobs: jobDefinitions(project.jobs ?? {}),
27
30
  options: {
28
31
  onUnhandledError: (error) => console.error(error),
29
32
  ...project.options
package/dist/dev/run.js CHANGED
@@ -5,6 +5,7 @@ import { moduleFile } from "../module-file.js";
5
5
  import { layoutCheck } from "../build/layout-check.js";
6
6
  import { projectUiPlugin } from "../project/project-ui.js";
7
7
  import { checkDatabase } from "./check-database.js";
8
+ import { stopDevJobs } from "./dev-jobs.js";
8
9
  import { existsSync } from "node:fs";
9
10
  import path from "node:path";
10
11
  import { createServer, searchForWorkspaceRoot } from "vite";
@@ -23,6 +24,10 @@ const reloadOnProjectChange = (dirs) => ({
23
24
  if (dirs.some((dir) => file.startsWith(`${dir}${path.sep}`)) && !file.includes(`${path.sep}node_modules${path.sep}`)) server.environments.ssr.moduleGraph.invalidateAll();
24
25
  }
25
26
  });
27
+ const stopJobsOnClose = {
28
+ name: "protobase-jobs",
29
+ closeBundle: () => stopDevJobs()
30
+ };
26
31
  const appDependencies = [
27
32
  "react",
28
33
  "react/jsx-runtime",
@@ -65,6 +70,7 @@ const runDev = async (options, out) => {
65
70
  },
66
71
  plugins: [
67
72
  reloadOnProjectChange([options.projectDir, ...extensions.map((extension) => extension.dir)]),
73
+ stopJobsOnClose,
68
74
  projectUiPlugin(options.projectDir, extensions),
69
75
  extensionImports(options.projectDir, extensions),
70
76
  extensionStyles(extensions),
package/dist/main.js CHANGED
@@ -9,8 +9,10 @@ import { parseHosts } from "./dev/hosts.js";
9
9
  import { runDev } from "./dev/run.js";
10
10
  import { runDoctor } from "./doctor/run.js";
11
11
  import { runScaffold } from "./scaffold/run.js";
12
+ import { closeOnSignals } from "./serve/signals.js";
12
13
  import { serveBundle } from "./serve/serve-bundle.js";
13
14
  import { serveBundleDir } from "./serve/serve-dir.js";
15
+ import { loadWorkerBundle, startWorker } from "./worker/start-worker.js";
14
16
  import { registerOrganizations } from "./organizations/register.js";
15
17
  import { registerFiles } from "./files/register.js";
16
18
  import { registerMigration } from "./migration/register.js";
@@ -86,6 +88,15 @@ program.command("serve").description("Serve a bundle written by `protobase build
86
88
  if (statSync(bundle).isDirectory()) await serveBundleDir(bundle, process.env, out);
87
89
  else await serveBundle(bundle, process.env, out);
88
90
  });
91
+ program.command("worker").description("Run the jobs of a bundle from `protobase build` in a process of their own, until SIGTERM or SIGINT, as `serve` runs them beside the API").argument("<bundle>", "the bundle folder, e.g. dist, or its dist/protobase.config.js").action(async (bundle) => {
92
+ loadNearestEnvFile(process.cwd());
93
+ const worker = await startWorker({
94
+ project: await loadWorkerBundle(bundle),
95
+ databaseUrl: process.env.DATABASE_URL || void 0
96
+ });
97
+ closeOnSignals(worker.stop);
98
+ out(`protobase worker running ${worker.jobs} job${worker.jobs === 1 ? "" : "s"}\n`);
99
+ });
89
100
  registerUsers(program);
90
101
  registerOrganizations(program);
91
102
  registerToken(program);
@@ -3,7 +3,7 @@ import { importProjectFile } from "../project/import-project-file.js";
3
3
  import { createPgDb } from "../project/pg-db.js";
4
4
  import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
- import { configExports, mergeConfig } from "@protobase/server";
6
+ import { configExports, jobDefinitions, mergeConfig } from "@protobase/server";
7
7
  import pg from "pg";
8
8
  //#region src/migration/load-project.ts
9
9
  const configOf = async (file) => (await importProjectFile(file)).default ?? {};
@@ -32,6 +32,7 @@ const loadMigrationProject = async (dir, extensions = []) => {
32
32
  db: project.db ?? ownDb,
33
33
  ...project.auth && { auth: project.auth },
34
34
  ...project.events && { events: project.events },
35
+ jobs: jobDefinitions(project.jobs ?? {}),
35
36
  close: async () => {
36
37
  await ownDb?.destroy();
37
38
  }
@@ -1,14 +1,22 @@
1
- import { authSchemaMigration, changesMigration } from "@protobase/server";
1
+ import { authSchemaMigration, changesMigration, jobsSchemaMigration } from "@protobase/server";
2
2
  //#region src/migration/parts.ts
3
3
  /** Every feature the project's config enables, in the order their files are written. */
4
- const migrationParts = (project) => [...project.auth ? [{
5
- name: "auth",
6
- title: "The auth schema changes this version of Protobase needs",
7
- sql: () => authSchemaMigration(project.auth)
8
- }] : [], {
9
- name: "changes",
10
- title: "The change capture the resources' events need",
11
- sql: () => changesMigration(project.db, project.models, project.events)
12
- }];
4
+ const migrationParts = (project) => [
5
+ ...project.auth ? [{
6
+ name: "auth",
7
+ title: "The auth schema changes this version of Protobase needs",
8
+ sql: () => authSchemaMigration(project.auth)
9
+ }] : [],
10
+ ...project.jobs.length > 0 ? [{
11
+ name: "jobs",
12
+ title: "The jobs schema this version of Protobase needs",
13
+ sql: () => jobsSchemaMigration(project.db)
14
+ }] : [],
15
+ {
16
+ name: "changes",
17
+ title: "The change capture the resources' events need",
18
+ sql: () => changesMigration(project.db, project.models, project.events)
19
+ }
20
+ ];
13
21
  //#endregion
14
22
  export { migrationParts };
@@ -7,7 +7,7 @@ import path from "node:path";
7
7
  const out = (text) => process.stdout.write(text);
8
8
  const collect = (value, previous) => [...previous, value];
9
9
  const registerMigration = (program) => {
10
- program.command("migration").description("Write what the database still needs for every feature the config enables (auth, events) as the project's next SQL migrations").option("--dir <dir>", "migrations folder", "db/migrations").option("--extend <dir>", "merge the protobase.config.ts in this folder before the project's, as dev and build do, repeatable", collect, []).action(async (opts) => {
10
+ program.command("migration").description("Write what the database still needs for every feature the config enables (auth, events, jobs) as the project's next SQL migrations").option("--dir <dir>", "migrations folder", "db/migrations").option("--extend <dir>", "merge the protobase.config.ts in this folder before the project's, as dev and build do, repeatable", collect, []).action(async (opts) => {
11
11
  const project = await loadMigrationProject(process.cwd(), resolveExtensions(opts.extend));
12
12
  try {
13
13
  const files = await writeMigrations(migrationParts(project), path.resolve(opts.dir));
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.8.1";
2
+ var version = "0.8.2";
3
3
  //#endregion
4
4
  export { version };
package/dist/serve/app.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { isApiPath } from "../bundle/manifest.js";
2
2
  import { serveStaticSite } from "./static-site.js";
3
- import { configExports, createAdmin } from "@protobase/server";
3
+ import { configExports, createAdmin, jobDefinitions, jobsDrainApp } from "@protobase/server";
4
4
  import { Hono } from "hono";
5
5
  import { logger } from "hono/logger";
6
6
  //#region src/serve/app.ts
7
- const createServeApp = ({ project, db, requestLog, site }) => {
7
+ const createServeApp = ({ project, db, requestLog, site, jobs, drainToken }) => {
8
8
  const { resources, views, pages, userMenu } = configExports(project.config);
9
9
  const admin = createAdmin({
10
10
  resources,
@@ -17,6 +17,7 @@ const createServeApp = ({ project, db, requestLog, site }) => {
17
17
  ...project.functions && { functions: project.functions },
18
18
  ...project.files && { files: project.files },
19
19
  ...project.events && { events: project.events },
20
+ jobs: jobDefinitions(project.jobs ?? {}),
20
21
  options: {
21
22
  onUnhandledError: (error) => console.error(error),
22
23
  ...project.options
@@ -26,6 +27,10 @@ const createServeApp = ({ project, db, requestLog, site }) => {
26
27
  if (requestLog) app.use(logger());
27
28
  app.get("/health", (c) => c.json({ status: "ok" }));
28
29
  if (site) app.use("*", (c, next) => isApiPath(site.api, c.req.path) ? next() : serveStaticSite(site, c.req.raw));
30
+ if (jobs && drainToken) app.route("/", jobsDrainApp({
31
+ runtime: jobs,
32
+ token: drainToken
33
+ }));
29
34
  app.route("/", admin);
30
35
  return app;
31
36
  };
package/dist/serve/env.js CHANGED
@@ -5,11 +5,15 @@ const readServeEnv = (env) => {
5
5
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`PORT must be a port number, not "${env.PORT}"`);
6
6
  const minutes = env.PROTOBASE_FILES_CLEANUP_MINUTES ? Number(env.PROTOBASE_FILES_CLEANUP_MINUTES) : 60;
7
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}"`);
8
+ const jobs = env.PROTOBASE_JOBS ?? "on";
9
+ if (jobs !== "on" && jobs !== "off") throw new Error(`PROTOBASE_JOBS must be on or off, not "${jobs}"`);
8
10
  return {
9
11
  port,
10
12
  databaseUrl: env.DATABASE_URL || void 0,
11
13
  requestLog: Boolean(env.REQUEST_LOG),
12
- filesCleanupMinutes: minutes
14
+ filesCleanupMinutes: minutes,
15
+ runJobs: jobs === "on",
16
+ ...env.PROTOBASE_JOBS_DRAIN_TOKEN && { drainToken: env.PROTOBASE_JOBS_DRAIN_TOKEN }
13
17
  };
14
18
  };
15
19
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { createPgDb } from "../project/pg-db.js";
2
2
  import { createServeApp } from "./app.js";
3
3
  import { startCleanupTimer } from "./cleanup-timer.js";
4
- import { checkAuthSchema, checkChanges, configExports, createFileCleanup } from "@protobase/server";
4
+ import { checkAuthSchema, checkChanges, checkJobsSchema, configExports, createFileCleanup, jobDefinitions, startJobs } from "@protobase/server";
5
5
  import { createAdaptorServer } from "@hono/node-server";
6
6
  import pg from "pg";
7
7
  //#region src/serve/lifecycle.ts
@@ -19,14 +19,35 @@ const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url
19
19
  const ownDb = project.db ? void 0 : createDb(env.databaseUrl);
20
20
  const db = project.db ?? ownDb;
21
21
  const { resources } = configExports(project.config);
22
- const app = createServeApp({
23
- project,
24
- db,
25
- requestLog: env.requestLog,
26
- site
27
- });
28
- const server = createAdaptorServer({ fetch: app.fetch });
29
- await checkChanges(db, resources.map((resource) => resource.toModel()), project.events).then(() => listen(server, env.port)).catch(async (error) => {
22
+ const definitions = jobDefinitions(project.jobs ?? {});
23
+ const startJobsAndListen = async () => {
24
+ await checkChanges(db, resources.map((resource) => resource.toModel()), project.events);
25
+ if (definitions.length > 0) await checkJobsSchema(db);
26
+ const jobs = definitions.length > 0 ? await startJobs({
27
+ db,
28
+ jobs: definitions,
29
+ work: env.runJobs,
30
+ onError: (error) => console.error(error)
31
+ }) : void 0;
32
+ const app = createServeApp({
33
+ project,
34
+ db,
35
+ requestLog: env.requestLog,
36
+ site,
37
+ ...jobs && { jobs },
38
+ ...env.drainToken && { drainToken: env.drainToken }
39
+ });
40
+ const server = createAdaptorServer({ fetch: app.fetch });
41
+ await listen(server, env.port).catch(async (error) => {
42
+ await jobs?.stop();
43
+ throw error;
44
+ });
45
+ return {
46
+ server,
47
+ jobs
48
+ };
49
+ };
50
+ const { server, jobs } = await startJobsAndListen().catch(async (error) => {
30
51
  await ownDb?.destroy();
31
52
  throw error;
32
53
  });
@@ -39,6 +60,7 @@ const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url
39
60
  const close = async () => {
40
61
  await closeServer(server);
41
62
  await timer?.stop();
63
+ await jobs?.stop();
42
64
  await ownDb?.destroy();
43
65
  };
44
66
  return {
@@ -0,0 +1,51 @@
1
+ import { readManifest } from "../bundle/manifest.js";
2
+ import { checkBundleVersion } from "../serve/bundle-version.js";
3
+ import { createPgDb } from "../project/pg-db.js";
4
+ import { loadBundle } from "../serve/load-bundle.js";
5
+ import { statSync } from "node:fs";
6
+ import path from "node:path";
7
+ import { checkJobsSchema, jobDefinitions, startJobs } from "@protobase/server";
8
+ import pg from "pg";
9
+ //#region src/worker/start-worker.ts
10
+ const bundleConfigFile = async (bundle) => {
11
+ if (!statSync(bundle).isDirectory()) return bundle;
12
+ return path.join(bundle, (await readManifest(bundle)).server);
13
+ };
14
+ /**
15
+ * Runs the project's jobs, and nothing else, until `stop`: the same handlers `protobase serve` runs, in a process of
16
+ * their own. It reads the jobs schema at start and refuses one that is missing or behind, like `serve`.
17
+ */
18
+ const startWorker = async ({ project, databaseUrl, createDb = (url) => createPgDb(pg, url) }) => {
19
+ const definitions = jobDefinitions(project.jobs ?? {});
20
+ if (definitions.length === 0) throw new Error("The bundle has no jobs: export them from protobase.config.ts as `jobs`");
21
+ if (!project.db && !databaseUrl) throw new Error("No database: set DATABASE_URL or export `db` from protobase.config.ts");
22
+ const ownDb = project.db ? void 0 : createDb(databaseUrl);
23
+ const db = project.db ?? ownDb;
24
+ const start = async () => {
25
+ await checkJobsSchema(db);
26
+ return startJobs({
27
+ db,
28
+ jobs: definitions,
29
+ onError: (error) => console.error(error)
30
+ });
31
+ };
32
+ const jobs = await start().catch(async (error) => {
33
+ await ownDb?.destroy();
34
+ throw error;
35
+ });
36
+ return {
37
+ jobs: definitions.length,
38
+ stop: async () => {
39
+ await jobs.stop();
40
+ await ownDb?.destroy();
41
+ }
42
+ };
43
+ };
44
+ /** `protobase worker <bundle>`: the bundle's jobs until SIGTERM or SIGINT, once its manifest shows a version this CLI runs. */
45
+ const loadWorkerBundle = async (bundle) => {
46
+ const file = await bundleConfigFile(bundle);
47
+ await checkBundleVersion(file);
48
+ return loadBundle(file);
49
+ };
50
+ //#endregion
51
+ export { bundleConfigFile, loadWorkerBundle, startWorker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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.8.1",
38
- "@protobase/query": "0.8.1",
39
- "@protobase/schema": "0.8.1",
40
- "@protobase/server": "0.8.1",
41
- "@protobase/ui": "0.8.1",
37
+ "@protobase/layout": "0.8.2",
38
+ "@protobase/query": "0.8.2",
39
+ "@protobase/schema": "0.8.2",
40
+ "@protobase/server": "0.8.2",
41
+ "@protobase/ui": "0.8.2",
42
42
  "@scalar/hono-api-reference": "^0.12.10",
43
43
  "@tailwindcss/vite": "^4.3.3",
44
44
  "@vitejs/plugin-react": "^6.1.2",