jazz-tools 2.0.0-alpha.21 → 2.0.0-alpha.24

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.
Files changed (226) hide show
  1. package/bin/docs-index.db +0 -0
  2. package/bin/docs-index.txt +1624 -542
  3. package/bin/jazz-tools.js +47 -41
  4. package/bin/native/jazz-tools-darwin-arm64 +0 -0
  5. package/bin/native/jazz-tools-darwin-x64 +0 -0
  6. package/bin/native/jazz-tools-linux-arm64 +0 -0
  7. package/bin/native/jazz-tools-linux-x64 +0 -0
  8. package/dist/backend/create-jazz-context.d.ts +31 -6
  9. package/dist/backend/create-jazz-context.d.ts.map +1 -1
  10. package/dist/backend/create-jazz-context.js +35 -5
  11. package/dist/backend/create-jazz-context.js.map +1 -1
  12. package/dist/backend/create-jazz-context.test.js +61 -6
  13. package/dist/backend/create-jazz-context.test.js.map +1 -1
  14. package/dist/cli.d.ts +29 -2
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +648 -246
  17. package/dist/cli.js.map +1 -1
  18. package/dist/cli.test.js +640 -294
  19. package/dist/cli.test.js.map +1 -1
  20. package/dist/codegen/schema-reader.d.ts.map +1 -1
  21. package/dist/codegen/schema-reader.js +6 -1
  22. package/dist/codegen/schema-reader.js.map +1 -1
  23. package/dist/dev-tools/dev-tools.d.ts.map +1 -1
  24. package/dist/dev-tools/dev-tools.js +61 -13
  25. package/dist/dev-tools/dev-tools.js.map +1 -1
  26. package/dist/dev-tools/dev-tools.test.js +166 -0
  27. package/dist/dev-tools/dev-tools.test.js.map +1 -1
  28. package/dist/dev-tools/extension-panel.d.ts.map +1 -1
  29. package/dist/dev-tools/extension-panel.js +30 -7
  30. package/dist/dev-tools/extension-panel.js.map +1 -1
  31. package/dist/dev-tools/protocol.d.ts +49 -1
  32. package/dist/dev-tools/protocol.d.ts.map +1 -1
  33. package/dist/dev-tools/protocol.js +3 -0
  34. package/dist/dev-tools/protocol.js.map +1 -1
  35. package/dist/drivers/index.d.ts +1 -1
  36. package/dist/drivers/index.d.ts.map +1 -1
  37. package/dist/drivers/schema-wire.d.ts.map +1 -1
  38. package/dist/drivers/schema-wire.js +12 -1
  39. package/dist/drivers/schema-wire.js.map +1 -1
  40. package/dist/drivers/schema-wire.test.d.ts +2 -0
  41. package/dist/drivers/schema-wire.test.d.ts.map +1 -0
  42. package/dist/drivers/schema-wire.test.js +31 -0
  43. package/dist/drivers/schema-wire.test.js.map +1 -0
  44. package/dist/drivers/types.d.ts +2 -0
  45. package/dist/drivers/types.d.ts.map +1 -1
  46. package/dist/dsl.d.ts +139 -95
  47. package/dist/dsl.d.ts.map +1 -1
  48. package/dist/dsl.js +64 -8
  49. package/dist/dsl.js.map +1 -1
  50. package/dist/dsl.test.js +78 -8
  51. package/dist/dsl.test.js.map +1 -1
  52. package/dist/index.d.ts +32 -3
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +16 -3
  55. package/dist/index.js.map +1 -1
  56. package/dist/magic-columns.d.ts +3 -1
  57. package/dist/magic-columns.d.ts.map +1 -1
  58. package/dist/magic-columns.js +20 -4
  59. package/dist/magic-columns.js.map +1 -1
  60. package/dist/mcp/build-index.test.js +1 -1
  61. package/dist/migrations.d.ts +126 -0
  62. package/dist/migrations.d.ts.map +1 -0
  63. package/dist/migrations.js +112 -0
  64. package/dist/migrations.js.map +1 -0
  65. package/dist/permissions/index.test.js +35 -0
  66. package/dist/permissions/index.test.js.map +1 -1
  67. package/dist/react-native/create-jazz-client.test.js +62 -42
  68. package/dist/react-native/create-jazz-client.test.js.map +1 -1
  69. package/dist/react-native/jazz-rn-runtime-adapter.d.ts +18 -3
  70. package/dist/react-native/jazz-rn-runtime-adapter.d.ts.map +1 -1
  71. package/dist/react-native/jazz-rn-runtime-adapter.js +110 -6
  72. package/dist/react-native/jazz-rn-runtime-adapter.js.map +1 -1
  73. package/dist/react-native/jazz-rn-runtime-adapter.test.js +149 -4
  74. package/dist/react-native/jazz-rn-runtime-adapter.test.js.map +1 -1
  75. package/dist/reconcile-array.d.ts +29 -0
  76. package/dist/reconcile-array.d.ts.map +1 -0
  77. package/dist/reconcile-array.js +110 -0
  78. package/dist/reconcile-array.js.map +1 -0
  79. package/dist/reconcile-array.test.d.ts +2 -0
  80. package/dist/reconcile-array.test.d.ts.map +1 -0
  81. package/dist/reconcile-array.test.js +118 -0
  82. package/dist/reconcile-array.test.js.map +1 -0
  83. package/dist/runtime/client.d.ts +24 -20
  84. package/dist/runtime/client.d.ts.map +1 -1
  85. package/dist/runtime/client.for-request.test.js +8 -8
  86. package/dist/runtime/client.for-request.test.js.map +1 -1
  87. package/dist/runtime/client.js +58 -25
  88. package/dist/runtime/client.js.map +1 -1
  89. package/dist/runtime/client.mutations.test.js +72 -1
  90. package/dist/runtime/client.mutations.test.js.map +1 -1
  91. package/dist/runtime/cloud-server.integration.test.js +145 -88
  92. package/dist/runtime/cloud-server.integration.test.js.map +1 -1
  93. package/dist/runtime/db.d.ts +3 -7
  94. package/dist/runtime/db.d.ts.map +1 -1
  95. package/dist/runtime/db.js +16 -14
  96. package/dist/runtime/db.js.map +1 -1
  97. package/dist/runtime/db.schema-order.test.js +8 -8
  98. package/dist/runtime/db.schema-order.test.js.map +1 -1
  99. package/dist/runtime/index.d.ts +1 -1
  100. package/dist/runtime/index.d.ts.map +1 -1
  101. package/dist/runtime/index.js +1 -1
  102. package/dist/runtime/index.js.map +1 -1
  103. package/dist/runtime/napi.integration.test.js +113 -136
  104. package/dist/runtime/napi.integration.test.js.map +1 -1
  105. package/dist/runtime/query-adapter.d.ts.map +1 -1
  106. package/dist/runtime/query-adapter.js +22 -2
  107. package/dist/runtime/query-adapter.js.map +1 -1
  108. package/dist/runtime/query-adapter.test.js +81 -5
  109. package/dist/runtime/query-adapter.test.js.map +1 -1
  110. package/dist/runtime/row-transformer.js +2 -2
  111. package/dist/runtime/row-transformer.js.map +1 -1
  112. package/dist/runtime/row-transformer.test.js +9 -9
  113. package/dist/runtime/row-transformer.test.js.map +1 -1
  114. package/dist/runtime/schema-fetch.d.ts +103 -1
  115. package/dist/runtime/schema-fetch.d.ts.map +1 -1
  116. package/dist/runtime/schema-fetch.js +106 -0
  117. package/dist/runtime/schema-fetch.js.map +1 -1
  118. package/dist/runtime/sync-transport.d.ts +3 -1
  119. package/dist/runtime/sync-transport.d.ts.map +1 -1
  120. package/dist/runtime/sync-transport.js +34 -3
  121. package/dist/runtime/sync-transport.js.map +1 -1
  122. package/dist/runtime/sync-transport.test.js +53 -1
  123. package/dist/runtime/sync-transport.test.js.map +1 -1
  124. package/dist/runtime/value-converter.d.ts +9 -6
  125. package/dist/runtime/value-converter.d.ts.map +1 -1
  126. package/dist/runtime/value-converter.js +22 -9
  127. package/dist/runtime/value-converter.js.map +1 -1
  128. package/dist/runtime/value-converter.test.js +32 -26
  129. package/dist/runtime/value-converter.test.js.map +1 -1
  130. package/dist/schema-loader.d.ts +14 -0
  131. package/dist/schema-loader.d.ts.map +1 -0
  132. package/dist/schema-loader.js +219 -0
  133. package/dist/schema-loader.js.map +1 -0
  134. package/dist/schema-permissions.d.ts +8 -0
  135. package/dist/schema-permissions.d.ts.map +1 -0
  136. package/dist/schema-permissions.js +266 -0
  137. package/dist/schema-permissions.js.map +1 -0
  138. package/dist/schema-permissions.test.d.ts +2 -0
  139. package/dist/schema-permissions.test.d.ts.map +1 -0
  140. package/dist/schema-permissions.test.js +43 -0
  141. package/dist/schema-permissions.test.js.map +1 -0
  142. package/dist/schema.d.ts +11 -9
  143. package/dist/schema.d.ts.map +1 -1
  144. package/dist/svelte/context.svelte.test.js +50 -0
  145. package/dist/svelte/rune-patterns.svelte.test.js +301 -0
  146. package/dist/svelte/test-helpers.svelte.js +14 -0
  147. package/dist/svelte/use-all.svelte.d.ts.map +1 -1
  148. package/dist/svelte/use-all.svelte.js +7 -1
  149. package/dist/testing/fixtures/basic/schema.d.ts +11 -0
  150. package/dist/testing/fixtures/basic/schema.d.ts.map +1 -0
  151. package/dist/testing/fixtures/basic/schema.js +10 -0
  152. package/dist/testing/fixtures/basic/schema.js.map +1 -0
  153. package/dist/testing/index.d.ts +2 -1
  154. package/dist/testing/index.d.ts.map +1 -1
  155. package/dist/testing/index.js +2 -1
  156. package/dist/testing/index.js.map +1 -1
  157. package/dist/testing/index.test.js +109 -9
  158. package/dist/testing/index.test.js.map +1 -1
  159. package/dist/testing/local-jazz-server.d.ts +2 -0
  160. package/dist/testing/local-jazz-server.d.ts.map +1 -1
  161. package/dist/testing/local-jazz-server.js +21 -51
  162. package/dist/testing/local-jazz-server.js.map +1 -1
  163. package/dist/testing/policy-test-app.d.ts.map +1 -1
  164. package/dist/testing/policy-test-app.js +71 -3
  165. package/dist/testing/policy-test-app.js.map +1 -1
  166. package/dist/typed-app.d.ts +364 -0
  167. package/dist/typed-app.d.ts.map +1 -0
  168. package/dist/{testing/fixtures/basic/app.js → typed-app.js} +118 -30
  169. package/dist/typed-app.js.map +1 -0
  170. package/dist/vue/use-all.d.ts +2 -2
  171. package/dist/vue/use-all.d.ts.map +1 -1
  172. package/dist/vue/use-all.js +9 -3
  173. package/dist/vue/use-all.js.map +1 -1
  174. package/dist/vue/use-all.test.js +137 -0
  175. package/dist/vue/use-all.test.js.map +1 -1
  176. package/package.json +17 -14
  177. package/bin/native/jazz-tools-windows-x64.exe +0 -0
  178. package/dist/codegen/codegen.test.d.ts +0 -2
  179. package/dist/codegen/codegen.test.d.ts.map +0 -1
  180. package/dist/codegen/codegen.test.js +0 -1134
  181. package/dist/codegen/codegen.test.js.map +0 -1
  182. package/dist/codegen/index.d.ts +0 -18
  183. package/dist/codegen/index.d.ts.map +0 -1
  184. package/dist/codegen/index.js +0 -22
  185. package/dist/codegen/index.js.map +0 -1
  186. package/dist/codegen/query-builder-generator.d.ts +0 -26
  187. package/dist/codegen/query-builder-generator.d.ts.map +0 -1
  188. package/dist/codegen/query-builder-generator.js +0 -377
  189. package/dist/codegen/query-builder-generator.js.map +0 -1
  190. package/dist/codegen/type-generator.d.ts +0 -30
  191. package/dist/codegen/type-generator.d.ts.map +0 -1
  192. package/dist/codegen/type-generator.js +0 -368
  193. package/dist/codegen/type-generator.js.map +0 -1
  194. package/dist/runtime/napi.fjall.db.all.integration.test.d.ts +0 -2
  195. package/dist/runtime/napi.fjall.db.all.integration.test.d.ts.map +0 -1
  196. package/dist/runtime/napi.fjall.db.all.integration.test.js +0 -76
  197. package/dist/runtime/napi.fjall.db.all.integration.test.js.map +0 -1
  198. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts +0 -2
  199. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts.map +0 -1
  200. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js +0 -47
  201. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js.map +0 -1
  202. package/dist/runtime/napi.fjall.test-helpers.d.ts +0 -34
  203. package/dist/runtime/napi.fjall.test-helpers.d.ts.map +0 -1
  204. package/dist/runtime/napi.fjall.test-helpers.js +0 -172
  205. package/dist/runtime/napi.fjall.test-helpers.js.map +0 -1
  206. package/dist/sql-gen.d.ts +0 -5
  207. package/dist/sql-gen.d.ts.map +0 -1
  208. package/dist/sql-gen.js +0 -234
  209. package/dist/sql-gen.js.map +0 -1
  210. package/dist/sql-gen.test.d.ts +0 -2
  211. package/dist/sql-gen.test.d.ts.map +0 -1
  212. package/dist/sql-gen.test.js +0 -481
  213. package/dist/sql-gen.test.js.map +0 -1
  214. package/dist/svelte/context.test.d.ts +0 -2
  215. package/dist/svelte/context.test.d.ts.map +0 -1
  216. package/dist/svelte/context.test.js +0 -55
  217. package/dist/svelte/use-all.test.d.ts +0 -2
  218. package/dist/svelte/use-all.test.d.ts.map +0 -1
  219. package/dist/svelte/use-all.test.js +0 -147
  220. package/dist/testing/fixtures/basic/app.d.ts +0 -59
  221. package/dist/testing/fixtures/basic/app.d.ts.map +0 -1
  222. package/dist/testing/fixtures/basic/app.js.map +0 -1
  223. package/dist/testing/fixtures/basic/current.d.ts +0 -2
  224. package/dist/testing/fixtures/basic/current.d.ts.map +0 -1
  225. package/dist/testing/fixtures/basic/current.js +0 -6
  226. package/dist/testing/fixtures/basic/current.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  // CLI for jazz-tools schema tooling
3
- import { spawn } from "child_process";
4
- import { access, readdir, writeFile } from "fs/promises";
5
- import { join, basename, dirname, resolve } from "path";
3
+ import { access, mkdir, readFile, readdir, writeFile } from "fs/promises";
4
+ import { basename, join, resolve } from "path";
6
5
  import { pathToFileURL } from "url";
7
- import { register as registerCjs } from "tsx/cjs/api";
8
6
  import { register as registerEsm } from "tsx/esm/api";
9
- import { schemaToSql, lensesToSql } from "./sql-gen.js";
10
- import { getCollectedSchema, getCollectedMigrations, resetCollectedState } from "./dsl.js";
11
- import { generateClient } from "./codegen/index.js";
7
+ import { schemaDefinitionToAst } from "./migrations.js";
8
+ import { loadCompiledSchema } from "./schema-loader.js";
9
+ import { encodePublishedMigrationValue, fetchPermissionsHead, fetchSchemaHashes, fetchStoredWasmSchema, publishStoredPermissions, publishStoredMigration, } from "./runtime/schema-fetch.js";
10
+ import { toValue } from "./runtime/value-converter.js";
11
+ const PERMISSIONS_LIFECYCLE_NOTE = "Permission-only changes do not create schema hashes or require migrations.";
12
12
  function parseArgs() {
13
13
  const args = process.argv.slice(2);
14
14
  const command = args[0] || "";
15
- let jazzBin = "jazz-tools";
16
- let schemaDir = join(process.cwd(), "schema");
15
+ let schemaDir = process.cwd();
16
+ let jazzBin;
17
17
  for (let i = 1; i < args.length; i++) {
18
18
  const arg = args[i];
19
19
  const nextArg = args[i + 1];
@@ -28,276 +28,612 @@ function parseArgs() {
28
28
  }
29
29
  return { command, options: { jazzBin, schemaDir } };
30
30
  }
31
- // Allow loading `.ts` schema files when invoked via `node dist/cli.js`.
32
31
  registerEsm();
33
- // Counter for cache-busting module loads.
34
32
  let importCounter = 0;
35
- function requirePermissionsModule(filePath) {
36
- const loader = registerCjs({ namespace: `jazz-tools-cli-permissions-${++importCounter}` });
33
+ async function pathExists(path) {
37
34
  try {
38
- return loader.require(resolve(filePath), import.meta.url);
35
+ await access(path);
36
+ return true;
39
37
  }
40
- finally {
41
- loader.unregister();
38
+ catch {
39
+ return false;
42
40
  }
43
41
  }
44
- async function loadSchemaModule(filePath) {
45
- resetCollectedState();
46
- const url = pathToFileURL(filePath).href + `?v=${++importCounter}`;
47
- await import(url);
42
+ export async function validate(options) {
43
+ const compiled = await loadCompiledSchema(options.schemaDir);
44
+ const tableCount = compiled.schema.tables.length;
45
+ console.log(`Loaded structural schema from ${compiled.schemaFile}.`);
46
+ if (compiled.permissionsFile) {
47
+ console.log(`Loaded current permissions from ${compiled.permissionsFile}.`);
48
+ console.log(PERMISSIONS_LIFECYCLE_NOTE);
49
+ console.log("Use `jazz-tools permissions status` or `jazz-tools permissions push` for auth publication.");
50
+ }
51
+ console.log(`Validated ${tableCount} table${tableCount === 1 ? "" : "s"} in schema.ts.`);
48
52
  }
49
- async function loadSchema(filePath) {
50
- await loadSchemaModule(filePath);
51
- return getCollectedSchema();
53
+ export async function exportSchema(options) {
54
+ if (options.format !== "json") {
55
+ throw new Error(`Unsupported schema export format: ${options.format}`);
56
+ }
57
+ const compiled = await loadCompiledSchema(options.schemaDir);
58
+ process.stdout.write(`${JSON.stringify(compiled.wasmSchema, null, 2)}\n`);
52
59
  }
53
- async function loadMigrationModule(filePath) {
54
- resetCollectedState();
55
- const url = pathToFileURL(filePath).href + `?v=${++importCounter}`;
56
- await import(url);
57
- return getCollectedMigrations();
58
- }
59
- async function generateSqlForSchemaFile(tsFile, schema) {
60
- const sql = schemaToSql(schema);
61
- const sqlFile = tsFile.replace(/\.ts$/, ".sql");
62
- await writeFile(sqlFile, sql);
63
- console.log(`Generated: ${basename(sqlFile)}`);
64
- }
65
- async function generateAppTs(schemaDir, schema) {
66
- const output = generateClient(schema);
67
- const appTsPath = join(schemaDir, "app.ts");
68
- await writeFile(appTsPath, output);
69
- console.log(`Generated: app.ts`);
70
- }
71
- /**
72
- * Check if a filename is a migration TypeScript stub.
73
- *
74
- * Valid format: `migration_v1_v2_455a1f10a158_357c464c4c43.ts`
75
- */
76
- function isMigrationTsStub(filename) {
77
- const pattern = /^migration_v\d+_v\d+_[0-9a-f]{12}_[0-9a-f]{12}\.ts$/;
78
- return pattern.test(filename);
79
- }
80
- /**
81
- * Generate migration SQL filename with direction before hashes.
82
- *
83
- * Input: migration_v1_v2_455a1f10a158_357c464c4c43.ts
84
- * Output: migration_v1_v2_fwd_455a1f10a158_357c464c4c43.sql
85
- */
86
- function migrationSqlFilename(tsFile, direction) {
87
- const dir = tsFile.substring(0, tsFile.lastIndexOf("/") + 1);
88
- const name = basename(tsFile, ".ts");
89
- const match = name.match(/^(migration_v\d+_v\d+)_([0-9a-f]{12})_([0-9a-f]{12})$/);
90
- if (!match) {
91
- return tsFile.replace(/\.ts$/, `_${direction}.sql`);
60
+ const SHORT_SCHEMA_HASH_LENGTH = 12;
61
+ function getFlagValue(args, flag) {
62
+ for (let i = 0; i < args.length; i++) {
63
+ const arg = args[i];
64
+ if (!arg) {
65
+ continue;
66
+ }
67
+ if (arg === flag) {
68
+ return args[i + 1];
69
+ }
70
+ const prefix = `${flag}=`;
71
+ if (arg.startsWith(prefix)) {
72
+ return arg.slice(prefix.length);
73
+ }
92
74
  }
93
- const [, prefix, hash1, hash2] = match;
94
- return `${dir}${prefix}_${direction}_${hash1}_${hash2}.sql`;
75
+ return undefined;
95
76
  }
96
- async function generateSqlForMigrationFile(tsFile) {
97
- const lenses = await loadMigrationModule(tsFile);
98
- if (lenses.length === 0) {
99
- console.error(`No migration found in ${basename(tsFile)}`);
100
- return;
77
+ function resolveMigrationOptions(args) {
78
+ const serverUrl = getFlagValue(args, "--server-url") ?? process.env.JAZZ_SERVER_URL;
79
+ const adminSecret = getFlagValue(args, "--admin-secret") ?? process.env.JAZZ_ADMIN_SECRET;
80
+ const migrationsDir = resolve(process.cwd(), getFlagValue(args, "--migrations-dir") ?? join(process.cwd(), "migrations"));
81
+ if (!serverUrl) {
82
+ throw new Error("Missing server URL. Pass --server-url <url> or set JAZZ_SERVER_URL.");
83
+ }
84
+ if (!adminSecret) {
85
+ throw new Error("Missing admin secret. Pass --admin-secret <secret> or set JAZZ_ADMIN_SECRET.");
101
86
  }
102
- const fwdSql = lensesToSql(lenses, "fwd");
103
- const bwdSql = lensesToSql(lenses, "bwd");
104
- const fwdFile = migrationSqlFilename(tsFile, "fwd");
105
- const bwdFile = migrationSqlFilename(tsFile, "bwd");
106
- await writeFile(fwdFile, fwdSql);
107
- await writeFile(bwdFile, bwdSql);
108
- console.log(`Generated: ${basename(fwdFile)}`);
109
- console.log(`Generated: ${basename(bwdFile)}`);
110
- }
111
- function isOperationPolicyLike(input) {
112
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
87
+ return {
88
+ serverUrl,
89
+ adminSecret,
90
+ migrationsDir,
91
+ };
92
+ }
93
+ function resolvePermissionsOptions(args) {
94
+ const serverUrl = getFlagValue(args, "--server-url") ?? process.env.JAZZ_SERVER_URL;
95
+ const adminSecret = getFlagValue(args, "--admin-secret") ?? process.env.JAZZ_ADMIN_SECRET;
96
+ const schemaDir = resolve(process.cwd(), getFlagValue(args, "--schema-dir") ?? process.cwd());
97
+ if (!serverUrl) {
98
+ throw new Error("Missing server URL. Pass --server-url <url> or set JAZZ_SERVER_URL.");
99
+ }
100
+ if (!adminSecret) {
101
+ throw new Error("Missing admin secret. Pass --admin-secret <secret> or set JAZZ_ADMIN_SECRET.");
102
+ }
103
+ return {
104
+ serverUrl,
105
+ adminSecret,
106
+ schemaDir,
107
+ };
108
+ }
109
+ function normalizeSchemaHashInput(hash, label) {
110
+ const normalized = hash.trim().toLowerCase();
111
+ if (!/^[0-9a-f]{12,64}$/.test(normalized)) {
112
+ throw new Error(`${label} must be a 12-64 character lowercase hex schema hash.`);
113
+ }
114
+ return normalized;
115
+ }
116
+ function shortSchemaHash(hash) {
117
+ return normalizeSchemaHashInput(hash, "schema hash").slice(0, SHORT_SCHEMA_HASH_LENGTH);
118
+ }
119
+ function hashMatchesFullSchema(hash, fullHash) {
120
+ return fullHash.startsWith(normalizeSchemaHashInput(hash, "schema hash"));
121
+ }
122
+ function resolveKnownSchemaHash(hash, label, knownHashes) {
123
+ const normalized = normalizeSchemaHashInput(hash, label);
124
+ if (normalized.length === 64) {
125
+ if (!knownHashes.includes(normalized)) {
126
+ throw new Error(`No stored schema found for ${label} ${normalized}.`);
127
+ }
128
+ return normalized;
129
+ }
130
+ const matches = knownHashes.filter((candidate) => candidate.startsWith(normalized));
131
+ if (matches.length === 0) {
132
+ throw new Error(`No stored schema found for ${label} prefix ${normalized}.`);
133
+ }
134
+ if (matches.length > 1) {
135
+ throw new Error(`${label} prefix ${normalized} is ambiguous: ${matches
136
+ .map((candidate) => shortSchemaHash(candidate))
137
+ .join(", ")}`);
138
+ }
139
+ return matches[0];
140
+ }
141
+ function columnTypeSignature(columnType) {
142
+ return JSON.stringify(columnType);
143
+ }
144
+ function columnsEqual(left, right) {
145
+ return (left.name === right.name &&
146
+ left.nullable === right.nullable &&
147
+ left.references === right.references &&
148
+ columnTypeSignature(left.column_type) === columnTypeSignature(right.column_type));
149
+ }
150
+ function tableSchemasEqual(left, right) {
151
+ if (!left || !right) {
152
+ return false;
153
+ }
154
+ if (left.columns.length !== right.columns.length) {
113
155
  return false;
114
156
  }
115
- const opPolicy = input;
116
- return Object.keys(opPolicy).every((key) => key === "using" || key === "with_check");
157
+ return left.columns.every((column, index) => columnsEqual(column, right.columns[index]));
117
158
  }
118
- function isTablePoliciesLike(input) {
119
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
159
+ function wasmSchemasEqual(left, right) {
160
+ const leftTableNames = Object.keys(left).sort();
161
+ const rightTableNames = Object.keys(right).sort();
162
+ if (leftTableNames.length !== rightTableNames.length) {
120
163
  return false;
121
164
  }
122
- const tablePolicy = input;
123
- const validOperationKeys = ["select", "insert", "update", "delete"];
124
- return Object.entries(tablePolicy).every(([key, value]) => {
125
- if (!validOperationKeys.includes(key)) {
165
+ return leftTableNames.every((tableName, index) => {
166
+ if (tableName !== rightTableNames[index]) {
126
167
  return false;
127
168
  }
128
- return isOperationPolicyLike(value);
169
+ return tableSchemasEqual(left[tableName], right[tableName]);
129
170
  });
130
171
  }
131
- function isPermissionsMap(input) {
132
- if (typeof input !== "object" || input === null) {
133
- return false;
172
+ function changedTableNames(fromSchema, toSchema) {
173
+ const names = new Set([...Object.keys(fromSchema), ...Object.keys(toSchema)]);
174
+ return [...names].filter((tableName) => !tableSchemasEqual(fromSchema[tableName], toSchema[tableName]));
175
+ }
176
+ function ensurePermissionsProject(compiled) {
177
+ if (!compiled.permissions || !compiled.permissionsFile) {
178
+ throw new Error("No permissions.ts found for this app. Create permissions.ts before using permissions commands.");
134
179
  }
135
- return Object.values(input).every((value) => isTablePoliciesLike(value));
180
+ return compiled;
136
181
  }
137
- async function loadPermissionsModule(filePath) {
138
- const module = requirePermissionsModule(filePath);
139
- const candidate = module.default ?? module.permissions ?? null;
140
- if (!candidate) {
141
- throw new Error(`Missing permissions export in ${basename(filePath)}. ` +
142
- `Export default definePermissions(...) (or export const permissions = definePermissions(...)).`);
182
+ async function resolveStoredStructuralSchemaHash(serverUrl, adminSecret, wasmSchema) {
183
+ const { hashes } = await fetchSchemaHashes(serverUrl, { adminSecret });
184
+ const storedSchemas = await Promise.all(hashes.map(async (hash) => ({
185
+ hash,
186
+ schema: (await fetchStoredWasmSchema(serverUrl, { adminSecret, schemaHash: hash })).schema,
187
+ })));
188
+ const match = storedSchemas.find(({ schema }) => wasmSchemasEqual(schema, wasmSchema));
189
+ if (!match) {
190
+ throw new Error("No stored structural schema matches the local schema.ts. Publish the structural schema before pushing permissions.");
143
191
  }
144
- if (!isPermissionsMap(candidate)) {
145
- throw new Error(`Invalid permissions export in ${basename(filePath)}. Expected default export from definePermissions(...).`);
192
+ return match.hash;
193
+ }
194
+ function pickWitnessSchema(schema, tableNames) {
195
+ return Object.fromEntries(tableNames
196
+ .filter((tableName) => schema[tableName])
197
+ .map((tableName) => [tableName, schema[tableName]]));
198
+ }
199
+ function indentBlock(text, indent) {
200
+ const prefix = " ".repeat(indent);
201
+ return text
202
+ .split("\n")
203
+ .map((line) => (line.length === 0 ? line : `${prefix}${line}`))
204
+ .join("\n");
205
+ }
206
+ function baseBuilderExpression(columnType, references) {
207
+ switch (columnType.type) {
208
+ case "Text":
209
+ return "s.string()";
210
+ case "Boolean":
211
+ return "s.boolean()";
212
+ case "Integer":
213
+ return "s.int()";
214
+ case "Double":
215
+ return "s.float()";
216
+ case "Timestamp":
217
+ return "s.timestamp()";
218
+ case "Bytea":
219
+ return "s.bytes()";
220
+ case "Json":
221
+ return columnType.schema ? `s.json(${JSON.stringify(columnType.schema)})` : "s.json()";
222
+ case "Enum":
223
+ return `s.enum(${columnType.variants.map((variant) => JSON.stringify(variant)).join(", ")})`;
224
+ case "Uuid":
225
+ if (!references) {
226
+ throw new Error("Migration stub generation does not yet support bare UUID columns.");
227
+ }
228
+ return `s.ref(${JSON.stringify(references)})`;
229
+ case "Array":
230
+ return `s.array(${baseBuilderExpression(columnType.element, references)})`;
231
+ case "BigInt":
232
+ throw new Error("Migration stub generation does not yet support BIGINT columns.");
233
+ case "Row":
234
+ throw new Error("Migration stub generation does not yet support row-valued columns.");
146
235
  }
147
- return candidate;
148
236
  }
149
- function mergePermissionsIntoSchema(schema, compiledPermissions) {
150
- const schemaTableNames = new Set(schema.tables.map((table) => table.name));
151
- const unknownTables = Object.keys(compiledPermissions).filter((tableName) => !schemaTableNames.has(tableName));
152
- if (unknownTables.length > 0) {
153
- throw new Error(`permissions.ts defines permissions for unknown table(s): ${unknownTables.join(", ")}.`);
237
+ function builderExpressionForColumn(column) {
238
+ const base = baseBuilderExpression(column.column_type, column.references);
239
+ return column.nullable ? `${base}.optional()` : base;
240
+ }
241
+ function sqlTypeToWasmColumnType(sqlType) {
242
+ if (typeof sqlType === "string") {
243
+ switch (sqlType) {
244
+ case "TEXT":
245
+ return { type: "Text" };
246
+ case "BOOLEAN":
247
+ return { type: "Boolean" };
248
+ case "INTEGER":
249
+ return { type: "Integer" };
250
+ case "REAL":
251
+ return { type: "Double" };
252
+ case "TIMESTAMP":
253
+ return { type: "Timestamp" };
254
+ case "UUID":
255
+ return { type: "Uuid" };
256
+ case "BYTEA":
257
+ return { type: "Bytea" };
258
+ }
259
+ }
260
+ if (sqlType.kind === "ENUM") {
261
+ return {
262
+ type: "Enum",
263
+ variants: [...sqlType.variants],
264
+ };
265
+ }
266
+ if (sqlType.kind === "JSON") {
267
+ return {
268
+ type: "Json",
269
+ schema: sqlType.schema,
270
+ };
154
271
  }
155
272
  return {
156
- tables: schema.tables.map((table) => {
157
- const external = compiledPermissions[table.name];
158
- if (!external) {
159
- return table;
273
+ type: "Array",
274
+ element: sqlTypeToWasmColumnType(sqlType.element),
275
+ };
276
+ }
277
+ function serializeForwardLenses(forward) {
278
+ return forward.map((tableLens) => ({
279
+ table: tableLens.table,
280
+ operations: tableLens.operations.map((op) => {
281
+ if (op.type === "rename") {
282
+ return op;
160
283
  }
284
+ const columnType = sqlTypeToWasmColumnType(op.sqlType);
285
+ const value = encodePublishedMigrationValue(toValue(op.value, columnType));
161
286
  return {
162
- ...table,
163
- policies: external,
287
+ type: op.type,
288
+ column: op.column,
289
+ columnType,
290
+ value,
164
291
  };
165
292
  }),
166
- };
293
+ }));
167
294
  }
168
- /**
169
- * Check if a path exists
170
- */
171
- const pathExists = async (path) => {
172
- try {
173
- await access(path);
174
- return true;
295
+ function renderSchemaWitness(schema) {
296
+ const tableEntries = Object.entries(schema)
297
+ .sort(([left], [right]) => left.localeCompare(right))
298
+ .map(([tableName, tableSchema]) => {
299
+ const columnLines = tableSchema.columns.map((column) => `${JSON.stringify(column.name)}: ${builderExpressionForColumn(column)},`);
300
+ return `${JSON.stringify(tableName)}: s.table({\n${indentBlock(columnLines.join("\n"), 2)}\n})`;
301
+ });
302
+ if (tableEntries.length === 0) {
303
+ return "{}";
175
304
  }
176
- catch {
177
- return false;
305
+ return `{\n${indentBlock(tableEntries.join(",\n"), 2)}\n}`;
306
+ }
307
+ function renderArrayElementExpression(columnType, references) {
308
+ return baseBuilderExpression(columnType, references);
309
+ }
310
+ function renderAddOperationExpression(column, defaultExpression) {
311
+ switch (column.column_type.type) {
312
+ case "Text":
313
+ return `s.add.string({ default: ${defaultExpression} })`;
314
+ case "Boolean":
315
+ return `s.add.boolean({ default: ${defaultExpression} })`;
316
+ case "Integer":
317
+ return `s.add.int({ default: ${defaultExpression} })`;
318
+ case "Double":
319
+ return `s.add.float({ default: ${defaultExpression} })`;
320
+ case "Timestamp":
321
+ return `s.add.timestamp({ default: ${defaultExpression} })`;
322
+ case "Bytea":
323
+ return `s.add.bytes({ default: ${defaultExpression} })`;
324
+ case "Json":
325
+ return column.column_type.schema
326
+ ? `s.add.json({ default: ${defaultExpression}, schema: ${JSON.stringify(column.column_type.schema)} })`
327
+ : `s.add.json({ default: ${defaultExpression} })`;
328
+ case "Enum":
329
+ return `s.add.enum(${column.column_type.variants
330
+ .map((variant) => JSON.stringify(variant))
331
+ .join(", ")}, { default: ${defaultExpression} })`;
332
+ case "Uuid":
333
+ if (column.references) {
334
+ return `s.add.ref(${JSON.stringify(column.references)}, { default: ${defaultExpression} })`;
335
+ }
336
+ return `s.add.ref("TODO_TABLE", { default: ${defaultExpression} })`;
337
+ case "Array":
338
+ return `s.add.array({ of: ${renderArrayElementExpression(column.column_type.element, column.references)}, default: ${defaultExpression} })`;
339
+ case "BigInt":
340
+ throw new Error("Migration stub generation does not yet support BIGINT columns.");
341
+ case "Row":
342
+ throw new Error("Migration stub generation does not yet support row-valued columns.");
343
+ }
344
+ }
345
+ function renderDropOperationExpression(column, defaultExpression) {
346
+ switch (column.column_type.type) {
347
+ case "Text":
348
+ return `s.drop.string({ backwardsDefault: ${defaultExpression} })`;
349
+ case "Boolean":
350
+ return `s.drop.boolean({ backwardsDefault: ${defaultExpression} })`;
351
+ case "Integer":
352
+ return `s.drop.int({ backwardsDefault: ${defaultExpression} })`;
353
+ case "Double":
354
+ return `s.drop.float({ backwardsDefault: ${defaultExpression} })`;
355
+ case "Timestamp":
356
+ return `s.drop.timestamp({ backwardsDefault: ${defaultExpression} })`;
357
+ case "Bytea":
358
+ return `s.drop.bytes({ backwardsDefault: ${defaultExpression} })`;
359
+ case "Json":
360
+ return column.column_type.schema
361
+ ? `s.drop.json({ backwardsDefault: ${defaultExpression}, schema: ${JSON.stringify(column.column_type.schema)} })`
362
+ : `s.drop.json({ backwardsDefault: ${defaultExpression} })`;
363
+ case "Enum":
364
+ return `s.drop.enum(${column.column_type.variants
365
+ .map((variant) => JSON.stringify(variant))
366
+ .join(", ")}, { backwardsDefault: ${defaultExpression} })`;
367
+ case "Uuid":
368
+ if (column.references) {
369
+ return `s.drop.ref(${JSON.stringify(column.references)}, { backwardsDefault: ${defaultExpression} })`;
370
+ }
371
+ return `s.drop.ref("TODO_TABLE", { backwardsDefault: ${defaultExpression} })`;
372
+ case "Array":
373
+ return `s.drop.array({ of: ${renderArrayElementExpression(column.column_type.element, column.references)}, backwardsDefault: ${defaultExpression} })`;
374
+ case "BigInt":
375
+ throw new Error("Migration stub generation does not yet support BIGINT columns.");
376
+ case "Row":
377
+ throw new Error("Migration stub generation does not yet support row-valued columns.");
178
378
  }
179
- };
180
- const findMonorepoJazzBinary = async () => {
181
- let currentDir = process.cwd();
182
- while (true) {
183
- const cargoTomlPath = join(currentDir, "Cargo.toml");
184
- const monorepoJazzToolsPath = join(currentDir, "target", "debug", "jazz-tools");
185
- const monorepoJazzPath = join(currentDir, "target", "debug", "jazz");
186
- if ((await pathExists(cargoTomlPath)) && (await pathExists(monorepoJazzToolsPath))) {
187
- return monorepoJazzToolsPath;
379
+ }
380
+ function inferTableSuggestions(tableName, fromTable, toTable) {
381
+ const fromColumns = new Map(fromTable.columns.map((column) => [column.name, column]));
382
+ const toColumns = new Map(toTable.columns.map((column) => [column.name, column]));
383
+ const comments = [];
384
+ const properties = [];
385
+ const removedColumns = [...fromColumns.keys()].filter((name) => !toColumns.has(name));
386
+ const addedColumns = [...toColumns.keys()].filter((name) => !fromColumns.has(name));
387
+ if (removedColumns.length === 1 && addedColumns.length === 1) {
388
+ const removed = fromColumns.get(removedColumns[0]);
389
+ const added = toColumns.get(addedColumns[0]);
390
+ if (removed.nullable === added.nullable &&
391
+ removed.references === added.references &&
392
+ columnTypeSignature(removed.column_type) === columnTypeSignature(added.column_type)) {
393
+ comments.push(`Possible rename detected: ${JSON.stringify(removed.name)} -> ${JSON.stringify(added.name)}.`);
188
394
  }
189
- // Backward compatibility for older local builds.
190
- if ((await pathExists(cargoTomlPath)) && (await pathExists(monorepoJazzPath))) {
191
- return monorepoJazzPath;
395
+ }
396
+ for (const columnName of addedColumns) {
397
+ const column = toColumns.get(columnName);
398
+ if (column.nullable) {
399
+ properties.push(`${JSON.stringify(columnName)}: ${renderAddOperationExpression(column, "null")},`);
192
400
  }
193
- const parentDir = dirname(currentDir);
194
- if (parentDir === currentDir) {
195
- return null;
401
+ else {
402
+ comments.push(`Added required column ${JSON.stringify(columnName)} needs an explicit default.`);
196
403
  }
197
- currentDir = parentDir;
198
404
  }
199
- };
200
- async function ensurePermissionsTestStub(schemaDir) {
201
- const permissionsFile = join(schemaDir, "permissions.ts");
202
- if (!(await pathExists(permissionsFile))) {
203
- return;
405
+ for (const columnName of removedColumns) {
406
+ const column = fromColumns.get(columnName);
407
+ if (column.nullable) {
408
+ properties.push(`${JSON.stringify(columnName)}: ${renderDropOperationExpression(column, "null")},`);
409
+ }
410
+ else {
411
+ comments.push(`Removed required column ${JSON.stringify(columnName)} needs an explicit backwardsDefault.`);
412
+ }
204
413
  }
205
- const testFile = join(schemaDir, "permissions.test.ts");
206
- if (await pathExists(testFile)) {
207
- return;
414
+ return {
415
+ tableName,
416
+ comments,
417
+ properties,
418
+ };
419
+ }
420
+ function renderMigrationBody(fromSchema, toSchema) {
421
+ const changedTables = changedTableNames(fromSchema, toSchema);
422
+ const migratableTables = changedTables.filter((tableName) => fromSchema[tableName] !== undefined && toSchema[tableName] !== undefined);
423
+ const witnessFrom = pickWitnessSchema(fromSchema, migratableTables);
424
+ const witnessTo = pickWitnessSchema(toSchema, migratableTables);
425
+ const lines = [];
426
+ for (const tableName of migratableTables) {
427
+ const fromTable = fromSchema[tableName];
428
+ const toTable = toSchema[tableName];
429
+ const suggestion = inferTableSuggestions(tableName, fromTable, toTable);
430
+ lines.push(`${JSON.stringify(tableName)}: {`);
431
+ for (const comment of suggestion.comments) {
432
+ lines.push(` // TODO: ${comment}`);
433
+ }
434
+ for (const property of suggestion.properties) {
435
+ lines.push(` ${property}`);
436
+ }
437
+ if (suggestion.comments.length === 0 && suggestion.properties.length === 0) {
438
+ lines.push(" // TODO: No safe migration steps were inferred automatically.");
439
+ }
440
+ lines.push("},");
441
+ lines.push("");
442
+ }
443
+ if (lines.length === 0) {
444
+ lines.push(changedTables.length === 0
445
+ ? "// TODO: No schema differences were detected."
446
+ : "// TODO: No column-level migration steps were required for the detected schema changes.");
208
447
  }
209
- const template = `/**
210
- * Permissions test starter.
211
- *
212
- * Suggested shape (jazz-tools/testing):
213
- * - createPolicyTestApp(...) for an isolated test app
214
- * - testApp.seed(...) to set up synthetic fixtures
215
- * - testApp.as(...) for request-scoped user clients
216
- * - testApp.expectAllowed(...) / testApp.expectDenied(...) assertions
217
- */
218
- import { describe, it } from "vitest";
448
+ return {
449
+ body: lines.join("\n").trimEnd(),
450
+ witnessFrom,
451
+ witnessTo,
452
+ };
453
+ }
454
+ async function packageVersion() {
455
+ const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
456
+ return packageJson.version ?? "unknown";
457
+ }
458
+ function createDateStamp(now = new Date()) {
459
+ const year = now.getFullYear();
460
+ const month = String(now.getMonth() + 1).padStart(2, "0");
461
+ const day = String(now.getDate()).padStart(2, "0");
462
+ return `${year}${month}${day}`;
463
+ }
464
+ function migrationFilename(migrationsDir, fromHash, toHash) {
465
+ return join(migrationsDir, `${createDateStamp()}-unnamed-${shortSchemaHash(fromHash)}-${shortSchemaHash(toHash)}.ts`);
466
+ }
467
+ function renderMigrationStub(input) {
468
+ const rendered = renderMigrationBody(input.fromSchema, input.toSchema);
469
+ return `import { schema as s } from "jazz-tools";
219
470
 
220
- describe.skip("permissions", () => {
221
- it("add policy tests", () => {
222
- // Starter file generated by jazz-tools build.
223
- });
471
+ export default s.defineMigration({
472
+ migrate: {
473
+ ${indentBlock(rendered.body, 4)}
474
+ },
475
+ fromHash: ${JSON.stringify(shortSchemaHash(input.fromHash))},
476
+ toHash: ${JSON.stringify(shortSchemaHash(input.toHash))},
477
+ from: ${renderSchemaWitness(rendered.witnessFrom)},
478
+ to: ${renderSchemaWitness(rendered.witnessTo)},
224
479
  });
225
480
  `;
226
- await writeFile(testFile, template);
227
- console.log(`Generated: permissions.test.ts`);
228
- }
229
- async function runJazzBuild(jazzBin, schemaDir, searchJazzBinOnError = true) {
230
- const result = await new Promise((resolve) => {
231
- console.log(`\nRunning: ${jazzBin} build --ts --schema-dir ${schemaDir}`);
232
- const child = spawn(jazzBin, ["build", "--ts", "--schema-dir", schemaDir], {
233
- stdio: "inherit",
234
- });
235
- child.on("close", (code) => {
236
- resolve({ type: "close", code });
237
- });
238
- child.on("error", (error) => {
239
- resolve({ type: "error", error });
240
- });
481
+ }
482
+ function isDefinedMigration(value) {
483
+ if (typeof value !== "object" || value === null) {
484
+ return false;
485
+ }
486
+ const candidate = value;
487
+ return (typeof candidate.fromHash === "string" &&
488
+ typeof candidate.toHash === "string" &&
489
+ typeof candidate.from === "object" &&
490
+ candidate.from !== null &&
491
+ typeof candidate.to === "object" &&
492
+ candidate.to !== null &&
493
+ Array.isArray(candidate.forward));
494
+ }
495
+ async function loadDefinedMigration(filePath) {
496
+ const url = pathToFileURL(filePath).href + `?v=${++importCounter}`;
497
+ const loaded = (await import(url));
498
+ const migration = loaded.default ?? loaded.migration;
499
+ if (!isDefinedMigration(migration)) {
500
+ throw new Error(`Invalid migration export in ${basename(filePath)}. Export default defineMigration(...).`);
501
+ }
502
+ return migration;
503
+ }
504
+ async function findMigrationFile(migrationsDir, fromHash, toHash) {
505
+ const fromShortHash = shortSchemaHash(fromHash);
506
+ const toShortHash = shortSchemaHash(toHash);
507
+ const files = await readdir(migrationsDir);
508
+ const matches = files
509
+ .filter((file) => file.endsWith(".ts"))
510
+ .filter((file) => file.includes(`-${fromShortHash}-${toShortHash}.ts`) ||
511
+ file.includes(`-${fromHash}-${toHash}.ts`));
512
+ if (matches.length === 0) {
513
+ throw new Error(`No migration file found in ${migrationsDir} for ${fromHash} -> ${toHash}.`);
514
+ }
515
+ if (matches.length > 1) {
516
+ throw new Error(`Multiple migration files found for ${fromHash} -> ${toHash}: ${matches.join(", ")}`);
517
+ }
518
+ return join(migrationsDir, matches[0]);
519
+ }
520
+ export async function createMigration(options) {
521
+ const { hashes } = await fetchSchemaHashes(options.serverUrl, {
522
+ adminSecret: options.adminSecret,
241
523
  });
242
- if (result.type === "close") {
243
- if (result.code !== 0) {
244
- console.warn(`jazz-tools build exited with code ${result.code} (versioned schemas not generated)`);
245
- }
524
+ const fromHash = resolveKnownSchemaHash(options.fromHash, "fromHash", hashes);
525
+ const toHash = resolveKnownSchemaHash(options.toHash, "toHash", hashes);
526
+ await mkdir(options.migrationsDir, { recursive: true });
527
+ const [{ schema: fromSchema }, { schema: toSchema }] = await Promise.all([
528
+ fetchStoredWasmSchema(options.serverUrl, {
529
+ adminSecret: options.adminSecret,
530
+ schemaHash: fromHash,
531
+ }),
532
+ fetchStoredWasmSchema(options.serverUrl, {
533
+ adminSecret: options.adminSecret,
534
+ schemaHash: toHash,
535
+ }),
536
+ ]);
537
+ const filePath = migrationFilename(options.migrationsDir, fromHash, toHash);
538
+ if (await pathExists(filePath)) {
539
+ throw new Error(`Migration stub already exists: ${filePath}`);
540
+ }
541
+ const stub = renderMigrationStub({ fromHash, toHash, fromSchema, toSchema });
542
+ await writeFile(filePath, stub);
543
+ const version = await packageVersion();
544
+ console.log(`Generated: ${filePath}`);
545
+ console.log("");
546
+ console.log("Migration stubs are only for structural schema changes.");
547
+ console.log(PERMISSIONS_LIFECYCLE_NOTE);
548
+ console.log("");
549
+ console.log("Next steps:");
550
+ console.log("1. Fill in migrate.");
551
+ console.log("2. Rename the file by replacing 'unnamed'.");
552
+ console.log(`3. Run npx jazz-tools@${version} migrations push ${shortSchemaHash(fromHash)} ${shortSchemaHash(toHash)}`);
553
+ return filePath;
554
+ }
555
+ export async function pushMigration(options) {
556
+ const { hashes } = await fetchSchemaHashes(options.serverUrl, {
557
+ adminSecret: options.adminSecret,
558
+ });
559
+ const fromHash = resolveKnownSchemaHash(options.fromHash, "fromHash", hashes);
560
+ const toHash = resolveKnownSchemaHash(options.toHash, "toHash", hashes);
561
+ const filePath = await findMigrationFile(options.migrationsDir, fromHash, toHash);
562
+ const migration = await loadDefinedMigration(filePath);
563
+ if (!hashMatchesFullSchema(migration.fromHash, fromHash) ||
564
+ !hashMatchesFullSchema(migration.toHash, toHash)) {
565
+ throw new Error(`Migration ${basename(filePath)} exports ${migration.fromHash} -> ${migration.toHash}, expected ${shortSchemaHash(fromHash)} -> ${shortSchemaHash(toHash)}.`);
566
+ }
567
+ schemaDefinitionToAst(migration.from);
568
+ schemaDefinitionToAst(migration.to);
569
+ if (migration.forward.length === 0) {
570
+ throw new Error(`Migration ${basename(filePath)} has no steps. Fill in migrate before push.`);
571
+ }
572
+ const forward = serializeForwardLenses(migration.forward);
573
+ await publishStoredMigration(options.serverUrl, {
574
+ adminSecret: options.adminSecret,
575
+ fromHash,
576
+ toHash,
577
+ forward,
578
+ });
579
+ console.log(`Pushed migration ${shortSchemaHash(fromHash)} -> ${shortSchemaHash(toHash)} from ${basename(filePath)}.`);
580
+ }
581
+ function describePermissionsHead(head) {
582
+ return `v${head.version} on ${shortSchemaHash(head.schemaHash)}`;
583
+ }
584
+ export async function permissionsStatus(options) {
585
+ const compiled = ensurePermissionsProject(await loadCompiledSchema(options.schemaDir));
586
+ const localSchemaHash = await resolveStoredStructuralSchemaHash(options.serverUrl, options.adminSecret, compiled.wasmSchema);
587
+ const { head } = await fetchPermissionsHead(options.serverUrl, {
588
+ adminSecret: options.adminSecret,
589
+ });
590
+ console.log(`Loaded structural schema from ${compiled.schemaFile}.`);
591
+ console.log(`Loaded current permissions from ${compiled.permissionsFile}.`);
592
+ console.log(`Local structural schema matches stored hash ${shortSchemaHash(localSchemaHash)}.`);
593
+ console.log(PERMISSIONS_LIFECYCLE_NOTE);
594
+ if (!head) {
595
+ console.log("Server has no published permissions head yet.");
596
+ console.log("Next push will publish version 1.");
246
597
  return;
247
598
  }
248
- const error = result.error;
249
- if (error.code === "ENOENT") {
250
- const monorepoJazzPath = searchJazzBinOnError ? await findMonorepoJazzBinary() : null;
251
- if (monorepoJazzPath) {
252
- console.log(`jazz-tools binary not found at '${jazzBin}'. Using monorepo binary at '${monorepoJazzPath}'`);
253
- return runJazzBuild(monorepoJazzPath, schemaDir, false);
254
- }
255
- else {
256
- console.warn(`jazz-tools binary not found at '${jazzBin}'. Use --jazz-bin to specify the path.\n` +
257
- `Versioned schemas will not be generated.`);
258
- }
599
+ console.log(`Server permissions head is ${describePermissionsHead(head)}.`);
600
+ if (head.schemaHash === localSchemaHash) {
601
+ console.log("Current server permissions already target this structural schema.");
259
602
  }
260
- console.warn(`jazz-tools build failed: ${error.message}`);
603
+ else {
604
+ console.log(`Current server permissions target ${shortSchemaHash(head.schemaHash)}; pushing will retarget the head to ${shortSchemaHash(localSchemaHash)}.`);
605
+ }
606
+ console.log(`Next push will require parent bundle ${head.bundleObjectId}.`);
261
607
  }
262
- export async function build(options) {
263
- const { jazzBin, schemaDir } = options;
264
- let files;
265
- try {
266
- files = await readdir(schemaDir);
608
+ export async function pushPermissions(options) {
609
+ const compiled = ensurePermissionsProject(await loadCompiledSchema(options.schemaDir));
610
+ const localSchemaHash = await resolveStoredStructuralSchemaHash(options.serverUrl, options.adminSecret, compiled.wasmSchema);
611
+ const { head: currentHead } = await fetchPermissionsHead(options.serverUrl, {
612
+ adminSecret: options.adminSecret,
613
+ });
614
+ const { head: publishedHead } = await publishStoredPermissions(options.serverUrl, {
615
+ adminSecret: options.adminSecret,
616
+ schemaHash: localSchemaHash,
617
+ permissions: compiled.permissions,
618
+ expectedParentBundleObjectId: currentHead?.bundleObjectId ?? null,
619
+ });
620
+ console.log(`Loaded structural schema from ${compiled.schemaFile}.`);
621
+ console.log(`Loaded current permissions from ${compiled.permissionsFile}.`);
622
+ console.log(`Resolved structural schema hash ${shortSchemaHash(localSchemaHash)}.`);
623
+ if (currentHead) {
624
+ console.log(`Publishing from parent ${describePermissionsHead(currentHead)}.`);
267
625
  }
268
- catch {
269
- console.error(`Schema directory not found: ${schemaDir}`);
270
- process.exit(1);
271
- }
272
- const tsFiles = files.filter((f) => f.endsWith(".ts"));
273
- for (const file of tsFiles.filter((name) => isMigrationTsStub(name))) {
274
- await generateSqlForMigrationFile(join(schemaDir, file));
275
- }
276
- const schemaFile = join(schemaDir, "current.ts");
277
- if (!(await pathExists(schemaFile))) {
278
- console.error(`Schema file not found: ${schemaFile}`);
279
- process.exit(1);
280
- }
281
- let schema = await loadSchema(schemaFile);
282
- const tablesWithInlinePolicies = schema.tables
283
- .filter((table) => table.policies)
284
- .map((t) => t.name);
285
- if (tablesWithInlinePolicies.length > 0) {
286
- throw new Error("Inline table permissions in current.ts are no longer supported. " +
287
- "Move policies to schema/permissions.ts. " +
288
- `Tables: ${tablesWithInlinePolicies.join(", ")}.`);
289
- }
290
- // Generate app.ts before loading permissions.ts so permissions can import it for typing.
291
- await generateAppTs(schemaDir, schema);
292
- const permissionsFile = join(schemaDir, "permissions.ts");
293
- if (await pathExists(permissionsFile)) {
294
- const permissions = await loadPermissionsModule(permissionsFile);
295
- schema = mergePermissionsIntoSchema(schema, permissions);
296
- }
297
- await generateSqlForSchemaFile(schemaFile, schema);
298
- await generateAppTs(schemaDir, schema);
299
- await ensurePermissionsTestStub(schemaDir);
300
- await runJazzBuild(jazzBin, schemaDir);
626
+ else {
627
+ console.log("Publishing first permissions head for this app.");
628
+ }
629
+ const nextHead = publishedHead ?? {
630
+ schemaHash: localSchemaHash,
631
+ version: currentHead ? currentHead.version + 1 : 1,
632
+ parentBundleObjectId: currentHead?.bundleObjectId ?? null,
633
+ bundleObjectId: currentHead?.bundleObjectId ?? "",
634
+ };
635
+ console.log(`Published permissions head ${describePermissionsHead(nextHead)}.`);
636
+ console.log(PERMISSIONS_LIFECYCLE_NOTE);
301
637
  }
302
638
  function isMainModule() {
303
639
  const entry = process.argv[1];
@@ -307,22 +643,88 @@ function isMainModule() {
307
643
  return pathToFileURL(entry).href === import.meta.url;
308
644
  }
309
645
  if (isMainModule()) {
310
- const { command, options } = parseArgs();
311
- switch (command) {
312
- case "build":
313
- build(options).catch((err) => {
314
- console.error(err.message);
315
- process.exit(1);
316
- });
317
- break;
318
- default:
319
- console.log("Usage: node <path-to-jazz-tools>/dist/cli.js build [options]");
320
- console.log("\nCommands:");
321
- console.log(" build Generate SQL from TypeScript schemas and run jazz-tools build");
322
- console.log("\nOptions:");
323
- console.log(" --jazz-bin <path> Path to jazz binary (default: jazz-tools)");
324
- console.log(" --schema-dir <path> Path to schema directory (default: ./schema)");
325
- process.exit(command ? 1 : 0);
646
+ const command = process.argv[2] ?? "";
647
+ if (command === "validate") {
648
+ const { options } = parseArgs();
649
+ validate(options).catch((err) => {
650
+ console.error(err.message);
651
+ process.exit(1);
652
+ });
653
+ }
654
+ else if (command === "schema") {
655
+ const subcommand = process.argv[3] ?? "";
656
+ if (subcommand !== "export") {
657
+ console.error("Usage: node dist/cli.js schema export [--schema-dir <path>] [--format json]");
658
+ process.exit(1);
659
+ }
660
+ const args = process.argv.slice(4);
661
+ const schemaDir = getFlagValue(args, "--schema-dir") ?? process.cwd();
662
+ const formatValue = getFlagValue(args, "--format") ?? "json";
663
+ if (formatValue !== "json") {
664
+ console.error(`Unsupported schema export format: ${formatValue}`);
665
+ process.exit(1);
666
+ }
667
+ exportSchema({ schemaDir, format: "json" }).catch((err) => {
668
+ console.error(err.message);
669
+ process.exit(1);
670
+ });
671
+ }
672
+ else if (command === "migrations") {
673
+ const subcommand = process.argv[3] ?? "";
674
+ const fromHash = process.argv[4];
675
+ const toHash = process.argv[5];
676
+ const sharedArgs = process.argv.slice(6);
677
+ if (!fromHash || !toHash) {
678
+ console.error("Usage: node dist/cli.js migrations <create|push> <fromHash> <toHash> [options]");
679
+ process.exit(1);
680
+ }
681
+ const options = resolveMigrationOptions(sharedArgs);
682
+ const task = subcommand === "create"
683
+ ? createMigration({ ...options, fromHash, toHash })
684
+ : subcommand === "push"
685
+ ? pushMigration({ ...options, fromHash, toHash })
686
+ : Promise.reject(new Error("Usage: node dist/cli.js migrations <create|push> <fromHash> <toHash> [options]"));
687
+ task.catch((err) => {
688
+ console.error(err.message);
689
+ process.exit(1);
690
+ });
691
+ }
692
+ else if (command === "permissions") {
693
+ const subcommand = process.argv[3] ?? "";
694
+ const options = resolvePermissionsOptions(process.argv.slice(4));
695
+ const task = subcommand === "status"
696
+ ? permissionsStatus(options)
697
+ : subcommand === "push"
698
+ ? pushPermissions(options)
699
+ : Promise.reject(new Error("Usage: node dist/cli.js permissions <status|push> [options]"));
700
+ task.catch((err) => {
701
+ console.error(err.message);
702
+ process.exit(1);
703
+ });
704
+ }
705
+ else {
706
+ console.log("Usage: node <path-to-jazz-tools>/dist/cli.js <command> [options]");
707
+ console.log("\nCommands:");
708
+ console.log(" validate Validate root schema.ts and optional permissions.ts");
709
+ console.log(" schema export Print the compiled structural schema as JSON");
710
+ console.log(" permissions status Show the current server permissions head for this app");
711
+ console.log(" permissions push Publish the current permissions.ts with head-parent checks");
712
+ console.log(" migrations create Generate a typed structural migration stub from two known schema hashes");
713
+ console.log(" migrations push Push a reviewed migration edge to the server");
714
+ console.log("\nValidation options:");
715
+ console.log(" --schema-dir <path> Path to app root containing schema.ts (default: .)");
716
+ console.log("\nSchema export options:");
717
+ console.log(" --schema-dir <path> Path to app root containing schema.ts (default: .)");
718
+ console.log(" --format json Output the compiled schema as JSON");
719
+ console.log("\nPermissions options:");
720
+ console.log(" --schema-dir <path> Path to app root containing schema.ts (default: .)");
721
+ console.log(" --server-url <url> Jazz server URL (or set JAZZ_SERVER_URL)");
722
+ console.log(" --admin-secret <sec> Admin secret (or set JAZZ_ADMIN_SECRET)");
723
+ console.log("\nMigration options:");
724
+ console.log(" --server-url <url> Jazz server URL (or set JAZZ_SERVER_URL)");
725
+ console.log(" --admin-secret <sec> Admin secret (or set JAZZ_ADMIN_SECRET)");
726
+ console.log(" --migrations-dir <p> Path to migrations directory (default: ./migrations)");
727
+ process.exit(command ? 1 : 0);
326
728
  }
327
729
  }
328
730
  //# sourceMappingURL=cli.js.map