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.test.js CHANGED
@@ -1,18 +1,19 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { chmod, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { access, chmod, copyFile, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
- import { afterEach, describe, expect, it } from "vitest";
7
- import { build } from "./cli.js";
7
+ import { afterEach, describe, expect, it, vi } from "vitest";
8
+ import { createMigration, exportSchema, permissionsStatus, pushMigration, pushPermissions, validate, } from "./cli.js";
8
9
  const dslPath = fileURLToPath(new URL("./dsl.ts", import.meta.url));
9
- const permissionsDslPath = fileURLToPath(new URL("./permissions/index.ts", import.meta.url));
10
- // Bin integration tests run in a subprocess that loads dist/cli.js, so current.ts must import
11
- // from the compiled dist to share the same dsl module instance (and thus _collectedSchema state).
12
- const distDslPath = fileURLToPath(new URL("../dist/dsl.js", import.meta.url));
13
- const distPermissionsDslPath = fileURLToPath(new URL("../dist/permissions/index.js", import.meta.url));
10
+ const indexPath = fileURLToPath(new URL("./index.ts", import.meta.url));
11
+ const distIndexPath = fileURLToPath(new URL("../dist/index.js", import.meta.url));
12
+ const binPath = fileURLToPath(new URL("../bin/jazz-tools.js", import.meta.url));
13
+ const bootstrapVerifierPath = fileURLToPath(new URL("../scripts/verify-packed-runtime-bootstrap.mjs", import.meta.url));
14
14
  const tempRoots = [];
15
15
  afterEach(async () => {
16
+ vi.unstubAllGlobals();
16
17
  await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
17
18
  });
18
19
  async function createWorkspace() {
@@ -20,44 +21,72 @@ async function createWorkspace() {
20
21
  tempRoots.push(root);
21
22
  const schemaDir = join(root, "schema");
22
23
  await mkdir(schemaDir, { recursive: true });
23
- const jazzBin = join(root, "fake-jazz");
24
- await writeFile(jazzBin, "#!/bin/sh\nexit 0\n");
25
- await chmod(jazzBin, 0o755);
26
- return { root, schemaDir, jazzBin };
24
+ await writeFile(join(root, "package.json"), '{ "type": "module" }\n');
25
+ return { root, schemaDir };
27
26
  }
28
- async function createFakeRustBin() {
29
- const root = await mkdtemp(join(tmpdir(), "jazz-tools-cli-rust-bin-"));
30
- tempRoots.push(root);
31
- const rustBin = join(root, "fake-jazz-tools");
32
- await writeFile(rustBin, "#!/bin/sh\nexit 0\n");
33
- await chmod(rustBin, 0o755);
34
- return rustBin;
27
+ async function fileExists(path) {
28
+ try {
29
+ await access(path);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ async function captureConsoleLogs(run) {
37
+ const logs = [];
38
+ const spy = vi
39
+ .spyOn(console, "log")
40
+ .mockImplementation((message, ...rest) => {
41
+ logs.push([message, ...rest].map((value) => String(value ?? "")).join(" "));
42
+ });
43
+ try {
44
+ const result = await run();
45
+ return { result, logs };
46
+ }
47
+ finally {
48
+ spy.mockRestore();
49
+ }
35
50
  }
36
- function currentSchemaWithoutInlinePermissions() {
51
+ function rootSchemaWithoutInlinePermissions(indexImportPath = indexPath) {
37
52
  return `
38
- import { table, col } from ${JSON.stringify(dslPath)};
53
+ import { schema as s } from ${JSON.stringify(indexImportPath)};
39
54
 
40
- table("projects", {
41
- name: col.string(),
42
- });
55
+ const schema = {
56
+ projects: s.table({
57
+ name: s.string(),
58
+ }),
59
+ todos: s.table({
60
+ title: s.string(),
61
+ ownerId: s.string(),
62
+ }),
63
+ };
43
64
 
44
- table("todos", {
45
- title: col.string(),
46
- ownerId: col.string(),
47
- });
65
+ type AppSchema = s.Schema<typeof schema>;
66
+ export const app: s.App<AppSchema> = s.defineApp(schema);
48
67
  `;
49
68
  }
50
- function currentSchemaWithInlinePermissions() {
69
+ function rootSchemaWithBooleanTodo(indexImportPath = indexPath) {
51
70
  return `
52
- import { table, col } from ${JSON.stringify(dslPath)};
71
+ import { schema as s } from ${JSON.stringify(indexImportPath)};
53
72
 
54
- table("projects", {
55
- name: col.string(),
56
- });
73
+ const schema = {
74
+ todos: s.table({
75
+ title: s.string(),
76
+ done: s.boolean(),
77
+ }),
78
+ };
79
+
80
+ type AppSchema = s.Schema<typeof schema>;
81
+ export const app: s.App<AppSchema> = s.defineApp(schema);
82
+ `;
83
+ }
84
+ function rootSchemaWithInlinePermissions(dslImportPath = dslPath) {
85
+ return `
86
+ import { table, col } from ${JSON.stringify(dslImportPath)};
57
87
 
58
88
  table("todos", {
59
89
  title: col.string(),
60
- ownerId: col.string(),
61
90
  }, {
62
91
  permissions: {
63
92
  select: { type: "True" },
@@ -65,16 +94,31 @@ table("todos", {
65
94
  });
66
95
  `;
67
96
  }
68
- function permissionsSchema(appImportPath = "./app.js") {
97
+ function rootPermissionsSchema(appImportPath = "./schema.ts", importPath = indexPath) {
98
+ return `
99
+ import { schema as s } from ${JSON.stringify(importPath)};
100
+ import { app } from ${JSON.stringify(appImportPath)};
101
+
102
+ export default s.definePermissions(app, ({ policy, session }) => [
103
+ policy.todos.allowRead.where({ ownerId: session.user_id }),
104
+ ]);
105
+ `;
106
+ }
107
+ function rootBooleanLiteralPermissionsSchema(appImportPath = "./schema.ts", importPath = indexPath) {
69
108
  return `
70
- import { definePermissions } from ${JSON.stringify(permissionsDslPath)};
109
+ import { schema as s } from ${JSON.stringify(importPath)};
71
110
  import { app } from ${JSON.stringify(appImportPath)};
72
111
 
73
- export default definePermissions(app, ({ policy, session }) => [
74
- policy.todos.allowRead.where({ owner_id: session.user_id }),
112
+ export default s.definePermissions(app, ({ policy }) => [
113
+ policy.todos.allowRead.where({ done: true }),
75
114
  ]);
76
115
  `;
77
116
  }
117
+ function permissionsSchemaMissingExport() {
118
+ return `
119
+ export const nope = 42;
120
+ `;
121
+ }
78
122
  function permissionsSchemaUnknownTable() {
79
123
  return `
80
124
  export default {
@@ -86,18 +130,13 @@ export default {
86
130
  };
87
131
  `;
88
132
  }
89
- function permissionsSchemaMissingExport() {
133
+ function permissionsSchemaNamedExport(appImportPath = "./schema.ts", importPath = indexPath) {
90
134
  return `
91
- export const nope = 42;
92
- `;
93
- }
94
- function permissionsSchemaNamedExport() {
95
- return `
96
- import { definePermissions } from ${JSON.stringify(permissionsDslPath)};
97
- import { app } from "./app.js";
135
+ import { schema as s } from ${JSON.stringify(importPath)};
136
+ import { app } from ${JSON.stringify(appImportPath)};
98
137
 
99
- export const permissions = definePermissions(app, ({ policy, session }) => [
100
- policy.todos.allowRead.where({ owner_id: session.user_id }),
138
+ export const permissions = s.definePermissions(app, ({ policy, session }) => [
139
+ policy.todos.allowRead.where({ ownerId: session.user_id }),
101
140
  ]);
102
141
  `;
103
142
  }
@@ -108,275 +147,582 @@ export default {
108
147
  };
109
148
  `;
110
149
  }
111
- // Bin integration variants — import from dist/dsl.js to share the module instance with dist/cli.js.
112
- function binCurrentSchema() {
113
- return `
114
- import { table, col } from ${JSON.stringify(distDslPath)};
115
-
116
- table("projects", {
117
- name: col.string(),
118
- });
119
-
120
- table("todos", {
121
- title: col.string(),
122
- ownerId: col.string(),
123
- });
124
- `;
150
+ function storedRootSchema() {
151
+ return {
152
+ projects: {
153
+ columns: [{ name: "name", column_type: { type: "Text" }, nullable: false }],
154
+ },
155
+ todos: {
156
+ columns: [
157
+ { name: "title", column_type: { type: "Text" }, nullable: false },
158
+ { name: "ownerId", column_type: { type: "Text" }, nullable: false },
159
+ ],
160
+ },
161
+ };
125
162
  }
126
- function binSchemaWithMessagesAndCanvases() {
127
- return `
128
- import { table, col } from ${JSON.stringify(distDslPath)};
129
-
130
- table("messages", {
131
- content: col.string(),
132
- isPublic: col.boolean(),
133
- });
134
-
135
- table("canvases", {
136
- name: col.string(),
137
- isPublic: col.boolean(),
138
- });
139
- `;
140
- }
141
- function binMigrationDropIsPublicFromBothTables() {
142
- return `
143
- import { migrate, col } from ${JSON.stringify(distDslPath)};
144
-
145
- migrate("messages", {
146
- isPublic: col.drop().boolean({ backwardsDefault: false }),
147
- });
148
-
149
- migrate("canvases", {
150
- isPublic: col.drop().boolean({ backwardsDefault: false }),
151
- });
152
- `;
153
- }
154
- function binPermissionsSchema(appImportPath = "./app.js") {
155
- return `
156
- import { definePermissions } from ${JSON.stringify(distPermissionsDslPath)};
157
- import { app } from ${JSON.stringify(appImportPath)};
158
-
159
- export default definePermissions(app, ({ policy, session }) => [
160
- policy.todos.allowRead.where({ owner_id: session.user_id }),
161
- ]);
162
- `;
163
- }
164
- function currentSchemaWithComments() {
165
- return `
166
- import { table, col } from ${JSON.stringify(distDslPath)};
167
-
168
- table("projects", {
169
- name: col.string(),
170
- });
171
-
172
- table("todos", {
173
- title: col.string(),
174
- ownerId: col.string(),
175
- });
176
-
177
- table("comments", {
178
- body: col.string(),
179
- });
180
- `;
181
- }
182
- function schemaWithMessagesAndCanvases() {
183
- return `
184
- import { table, col } from ${JSON.stringify(dslPath)};
185
-
186
- table("messages", {
187
- content: col.string(),
188
- isPublic: col.boolean(),
189
- });
190
-
191
- table("canvases", {
192
- name: col.string(),
193
- isPublic: col.boolean(),
194
- });
195
- `;
196
- }
197
- function migrationDropIsPublicFromBothTables() {
198
- return `
199
- import { migrate, col } from ${JSON.stringify(dslPath)};
200
-
201
- migrate("messages", {
202
- isPublic: col.drop().boolean({ backwardsDefault: false }),
203
- });
204
-
205
- migrate("canvases", {
206
- isPublic: col.drop().boolean({ backwardsDefault: false }),
207
- });
208
- `;
209
- }
210
- describe("cli build basic output", () => {
211
- it("generates app.ts even when current.sql already exists", async () => {
212
- const { schemaDir, jazzBin } = await createWorkspace();
213
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
214
- await writeFile(join(schemaDir, "current.sql"), "-- stale");
215
- await build({ schemaDir, jazzBin });
216
- await readFile(join(schemaDir, "app.ts"), "utf8");
163
+ describe("cli validate", () => {
164
+ it("validates root schema.ts without generating SQL or app artifacts", async () => {
165
+ const { root } = await createWorkspace();
166
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
167
+ await validate({ schemaDir: root });
168
+ expect(await fileExists(join(root, "schema", "current.sql"))).toBe(false);
169
+ expect(await fileExists(join(root, "schema", "app.ts"))).toBe(false);
170
+ expect(await fileExists(join(root, "permissions.test.ts"))).toBe(false);
217
171
  });
218
- });
219
- describe("cli build migration SQL generation", () => {
220
- it("generates DROP COLUMN for every table when migrate() is called on multiple tables", async () => {
221
- const { schemaDir, jazzBin } = await createWorkspace();
222
- await writeFile(join(schemaDir, "current.ts"), schemaWithMessagesAndCanvases());
223
- await writeFile(join(schemaDir, "migration_v1_v2_aaaaaaaaaaaa_bbbbbbbbbbbb.ts"), migrationDropIsPublicFromBothTables());
224
- await build({ schemaDir, jazzBin });
225
- const fwdSql = await readFile(join(schemaDir, "migration_v1_v2_fwd_aaaaaaaaaaaa_bbbbbbbbbbbb.sql"), "utf8");
226
- const bwdSql = await readFile(join(schemaDir, "migration_v1_v2_bwd_aaaaaaaaaaaa_bbbbbbbbbbbb.sql"), "utf8");
227
- expect(fwdSql).toContain("ALTER TABLE messages DROP COLUMN isPublic;");
228
- expect(fwdSql).toContain("ALTER TABLE canvases DROP COLUMN isPublic;");
229
- expect(bwdSql).toContain("ALTER TABLE messages ADD COLUMN isPublic BOOLEAN DEFAULT FALSE;");
230
- expect(bwdSql).toContain("ALTER TABLE canvases ADD COLUMN isPublic BOOLEAN DEFAULT FALSE;");
172
+ it("fails when pointed at the legacy ./schema shim directory", async () => {
173
+ const { root, schemaDir } = await createWorkspace();
174
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
175
+ await expect(validate({ schemaDir })).rejects.toThrow(/schema file not found/i);
176
+ });
177
+ it("loads root permissions.ts that imports ./schema.ts", async () => {
178
+ const { root } = await createWorkspace();
179
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
180
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema());
181
+ const { logs } = await captureConsoleLogs(() => validate({ schemaDir: root }));
182
+ expect(await fileExists(join(root, "schema", "current.sql"))).toBe(false);
183
+ expect(await fileExists(join(root, "permissions.test.ts"))).toBe(false);
184
+ expect(logs).toContain(`Loaded structural schema from ${join(root, "schema.ts")}.`);
185
+ expect(logs).toContain(`Loaded current permissions from ${join(root, "permissions.ts")}.`);
186
+ expect(logs).toContain("Permission-only changes do not create schema hashes or require migrations.");
187
+ });
188
+ it("loads src/schema.ts and src/permissions.ts when schemaDir points at the app root", async () => {
189
+ const { root } = await createWorkspace();
190
+ const srcDir = join(root, "src");
191
+ await mkdir(srcDir, { recursive: true });
192
+ await writeFile(join(srcDir, "schema.ts"), rootSchemaWithoutInlinePermissions());
193
+ await writeFile(join(srcDir, "permissions.ts"), rootPermissionsSchema());
194
+ const { logs } = await captureConsoleLogs(() => validate({ schemaDir: root }));
195
+ expect(logs).toContain(`Loaded structural schema from ${join(srcDir, "schema.ts")}.`);
196
+ expect(logs).toContain(`Loaded current permissions from ${join(srcDir, "permissions.ts")}.`);
197
+ });
198
+ it("accepts named permissions exports for transitional ergonomics", async () => {
199
+ const { root } = await createWorkspace();
200
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
201
+ await writeFile(join(root, "permissions.ts"), permissionsSchemaNamedExport());
202
+ await validate({ schemaDir: root });
203
+ });
204
+ it("fails when schema.ts uses inline table permissions", async () => {
205
+ const { root } = await createWorkspace();
206
+ await writeFile(join(root, "schema.ts"), rootSchemaWithInlinePermissions());
207
+ await expect(validate({ schemaDir: root })).rejects.toThrow(/inline table permissions are no longer supported/i);
208
+ });
209
+ it("fails when permissions.ts has no default or named permissions export", async () => {
210
+ const { root } = await createWorkspace();
211
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
212
+ await writeFile(join(root, "permissions.ts"), permissionsSchemaMissingExport());
213
+ await expect(validate({ schemaDir: root })).rejects.toThrow(/missing permissions export/i);
214
+ });
215
+ it("fails when permissions.ts references unknown tables", async () => {
216
+ const { root } = await createWorkspace();
217
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
218
+ await writeFile(join(root, "permissions.ts"), permissionsSchemaUnknownTable());
219
+ await expect(validate({ schemaDir: root })).rejects.toThrow(/permissions\.ts defines permissions for unknown table\(s\): ghosts/i);
220
+ });
221
+ it("fails when permissions.ts export shape is invalid", async () => {
222
+ const { root } = await createWorkspace();
223
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
224
+ await writeFile(join(root, "permissions.ts"), permissionsSchemaInvalidShape());
225
+ await expect(validate({ schemaDir: root })).rejects.toThrow(/invalid permissions export/i);
231
226
  });
232
227
  });
233
- describe("cli build permissions generation", () => {
234
- it("loads permissions.ts, merges policies, and creates permissions.test.ts stub", async () => {
235
- const { schemaDir, jazzBin } = await createWorkspace();
236
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
237
- await writeFile(join(schemaDir, "permissions.ts"), permissionsSchema());
238
- await build({ schemaDir, jazzBin });
239
- const sql = await readFile(join(schemaDir, "current.sql"), "utf8");
240
- const appTs = await readFile(join(schemaDir, "app.ts"), "utf8");
241
- const permissionsTest = await readFile(join(schemaDir, "permissions.test.ts"), "utf8");
242
- expect(sql).toContain("CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);");
243
- expect(appTs).toContain('"policies"');
244
- expect(appTs).toContain('"type": "SessionRef"');
245
- expect(appTs).toContain('"column": "owner_id"');
246
- expect(permissionsTest).toContain("Permissions test starter.");
228
+ describe("cli schema export", () => {
229
+ it("prints the compiled schema representation as JSON", async () => {
230
+ const { root } = await createWorkspace();
231
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
232
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema());
233
+ const writes = [];
234
+ const originalWrite = process.stdout.write.bind(process.stdout);
235
+ const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk) => {
236
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
237
+ return true;
238
+ }));
239
+ try {
240
+ await exportSchema({ schemaDir: root, format: "json" });
241
+ }
242
+ finally {
243
+ writeSpy.mockRestore();
244
+ process.stdout.write = originalWrite;
245
+ }
246
+ const exported = JSON.parse(writes.join(""));
247
+ expect(exported.projects.columns[0].name).toBe("name");
248
+ expect(exported.todos.columns.map((column) => column.name)).toEqual([
249
+ "title",
250
+ "ownerId",
251
+ ]);
252
+ expect(exported.todos.policies).toBeUndefined();
247
253
  });
248
- it("loads permissions.ts when it imports app from ./app.ts", async () => {
249
- const { schemaDir, jazzBin } = await createWorkspace();
250
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
251
- await writeFile(join(schemaDir, "permissions.ts"), permissionsSchema("./app.ts"));
252
- await build({ schemaDir, jazzBin });
253
- const sql = await readFile(join(schemaDir, "current.sql"), "utf8");
254
- expect(sql).toContain("CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);");
254
+ it("prints the compiled schema representation from src/schema.ts", async () => {
255
+ const { root } = await createWorkspace();
256
+ const srcDir = join(root, "src");
257
+ await mkdir(srcDir, { recursive: true });
258
+ await writeFile(join(srcDir, "schema.ts"), rootSchemaWithoutInlinePermissions());
259
+ await writeFile(join(srcDir, "permissions.ts"), rootPermissionsSchema());
260
+ const writes = [];
261
+ const originalWrite = process.stdout.write.bind(process.stdout);
262
+ const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk) => {
263
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
264
+ return true;
265
+ }));
266
+ try {
267
+ await exportSchema({ schemaDir: root, format: "json" });
268
+ }
269
+ finally {
270
+ writeSpy.mockRestore();
271
+ process.stdout.write = originalWrite;
272
+ }
273
+ const exported = JSON.parse(writes.join(""));
274
+ expect(exported.projects.columns[0].name).toBe("name");
275
+ expect(exported.todos.columns.map((column) => column.name)).toEqual([
276
+ "title",
277
+ "ownerId",
278
+ ]);
255
279
  });
256
- it("loads permissions.ts when it imports app from ./app", async () => {
257
- const { schemaDir, jazzBin } = await createWorkspace();
258
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
259
- await writeFile(join(schemaDir, "permissions.ts"), permissionsSchema("./app"));
260
- await build({ schemaDir, jazzBin });
261
- const sql = await readFile(join(schemaDir, "current.sql"), "utf8");
262
- expect(sql).toContain("CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);");
280
+ });
281
+ describe("cli migrations", () => {
282
+ it("generates a typed migration stub from stored schema hashes", async () => {
283
+ const { root } = await createWorkspace();
284
+ const migrationsDir = join(root, "migrations");
285
+ const fromHash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
286
+ const toHash = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
287
+ const fromShortHash = fromHash.slice(0, 12);
288
+ const toShortHash = toHash.slice(0, 12);
289
+ const fetchMock = vi.fn(async (input) => {
290
+ if (input.endsWith("/schemas")) {
291
+ return new Response(JSON.stringify({ hashes: [fromHash, toHash] }), { status: 200 });
292
+ }
293
+ if (input.endsWith(`/schema/${fromHash}`)) {
294
+ return new Response(JSON.stringify({
295
+ todos: {
296
+ columns: [{ name: "title", column_type: { type: "Text" }, nullable: false }],
297
+ },
298
+ }), { status: 200 });
299
+ }
300
+ if (input.endsWith(`/schema/${toHash}`)) {
301
+ return new Response(JSON.stringify({
302
+ todos: {
303
+ columns: [
304
+ { name: "title", column_type: { type: "Text" }, nullable: false },
305
+ { name: "notes", column_type: { type: "Text" }, nullable: true },
306
+ ],
307
+ },
308
+ }), { status: 200 });
309
+ }
310
+ throw new Error(`Unexpected fetch: ${input}`);
311
+ });
312
+ vi.stubGlobal("fetch", fetchMock);
313
+ const { result: filePath, logs } = await captureConsoleLogs(() => createMigration({
314
+ serverUrl: "http://localhost:1625",
315
+ adminSecret: "admin-secret",
316
+ migrationsDir,
317
+ fromHash: fromShortHash,
318
+ toHash: toShortHash,
319
+ }));
320
+ const generated = await readFile(filePath, "utf8");
321
+ expect(filePath).toContain(`-unnamed-${fromShortHash}-${toShortHash}.ts`);
322
+ expect(generated).toContain("s.defineMigration");
323
+ expect(generated).toContain(`fromHash: "${fromShortHash}"`);
324
+ expect(generated).toContain(`toHash: "${toShortHash}"`);
325
+ expect(generated).toContain("migrate: {");
326
+ expect(generated).toContain('"notes": s.add.string({ default: null }),');
327
+ expect(logs).toContain("Migration stubs are only for structural schema changes.");
328
+ expect(logs).toContain("Permission-only changes do not create schema hashes or require migrations.");
263
329
  });
264
- it("fails when current.ts uses inline table permissions", async () => {
265
- const { schemaDir, jazzBin } = await createWorkspace();
266
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithInlinePermissions());
267
- await expect(build({ schemaDir, jazzBin })).rejects.toThrow(/inline table permissions are no longer supported/i);
330
+ it("skips table add/drop steps when inferring a migration stub", async () => {
331
+ const { root } = await createWorkspace();
332
+ const migrationsDir = join(root, "migrations");
333
+ const fromHash = "abababababababababababababababababababababababababababababababab";
334
+ const toHash = "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd";
335
+ const fromShortHash = fromHash.slice(0, 12);
336
+ const toShortHash = toHash.slice(0, 12);
337
+ const fetchMock = vi.fn(async (input) => {
338
+ if (input.endsWith("/schemas")) {
339
+ return new Response(JSON.stringify({ hashes: [fromHash, toHash] }), { status: 200 });
340
+ }
341
+ if (input.endsWith(`/schema/${fromHash}`)) {
342
+ return new Response(JSON.stringify({
343
+ todos: {
344
+ columns: [{ name: "title", column_type: { type: "Text" }, nullable: false }],
345
+ },
346
+ legacy_users: {
347
+ columns: [{ name: "email", column_type: { type: "Text" }, nullable: false }],
348
+ },
349
+ }), { status: 200 });
350
+ }
351
+ if (input.endsWith(`/schema/${toHash}`)) {
352
+ return new Response(JSON.stringify({
353
+ todos: {
354
+ columns: [
355
+ { name: "title", column_type: { type: "Text" }, nullable: false },
356
+ { name: "notes", column_type: { type: "Text" }, nullable: true },
357
+ ],
358
+ },
359
+ users: {
360
+ columns: [{ name: "name", column_type: { type: "Text" }, nullable: false }],
361
+ },
362
+ }), { status: 200 });
363
+ }
364
+ throw new Error(`Unexpected fetch: ${input}`);
365
+ });
366
+ vi.stubGlobal("fetch", fetchMock);
367
+ const filePath = await createMigration({
368
+ serverUrl: "http://localhost:1625",
369
+ adminSecret: "admin-secret",
370
+ migrationsDir,
371
+ fromHash: fromShortHash,
372
+ toHash: toShortHash,
373
+ });
374
+ const generated = await readFile(filePath, "utf8");
375
+ expect(generated).toContain('"todos": {');
376
+ expect(generated).toContain('"notes": s.add.string({ default: null }),');
377
+ expect(generated).not.toContain("createTable");
378
+ expect(generated).not.toContain("dropTable");
379
+ expect(generated).not.toContain('"legacy_users"');
380
+ expect(generated).not.toContain('"users"');
268
381
  });
269
- it("does not overwrite an existing permissions.test.ts file", async () => {
270
- const { schemaDir, jazzBin } = await createWorkspace();
271
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
272
- await writeFile(join(schemaDir, "permissions.ts"), permissionsSchema());
273
- await writeFile(join(schemaDir, "permissions.test.ts"), "// keep-existing-test\n");
274
- await build({ schemaDir, jazzBin });
275
- const permissionsTest = await readFile(join(schemaDir, "permissions.test.ts"), "utf8");
276
- expect(permissionsTest).toBe("// keep-existing-test\n");
382
+ it("pushes a reviewed migration via the admin migrations endpoint", async () => {
383
+ const { root } = await createWorkspace();
384
+ const migrationsDir = join(root, "migrations");
385
+ await mkdir(migrationsDir, { recursive: true });
386
+ const fromHash = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
387
+ const toHash = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
388
+ const fromShortHash = fromHash.slice(0, 12);
389
+ const toShortHash = toHash.slice(0, 12);
390
+ const migrationPath = join(migrationsDir, `20260318-rename-${fromShortHash}-${toShortHash}.ts`);
391
+ await writeFile(migrationPath, `
392
+ import { schema as s } from ${JSON.stringify(indexPath)};
393
+
394
+ export default s.defineMigration({
395
+ migrate: {
396
+ users: {
397
+ email_address: s.renameFrom("email"),
398
+ },
399
+ },
400
+ fromHash: ${JSON.stringify(fromShortHash)},
401
+ toHash: ${JSON.stringify(toShortHash)},
402
+ from: {
403
+ users: s.table({
404
+ email: s.string(),
405
+ }),
406
+ },
407
+ to: {
408
+ users: s.table({
409
+ email_address: s.string(),
410
+ }),
411
+ },
412
+ });
413
+ `);
414
+ const fetchMock = vi.fn(async (_input, init) => {
415
+ if (_input.endsWith("/schemas")) {
416
+ return new Response(JSON.stringify({ hashes: [fromHash, toHash] }), { status: 200 });
417
+ }
418
+ const body = JSON.parse(String(init?.body));
419
+ expect(body.fromHash).toBe(fromHash);
420
+ expect(body.toHash).toBe(toHash);
421
+ expect(body.forward).toEqual([
422
+ {
423
+ table: "users",
424
+ operations: [
425
+ {
426
+ type: "rename",
427
+ column: "email",
428
+ value: "email_address",
429
+ },
430
+ ],
431
+ },
432
+ ]);
433
+ return new Response(JSON.stringify({ ok: true }), { status: 201 });
434
+ });
435
+ vi.stubGlobal("fetch", fetchMock);
436
+ await pushMigration({
437
+ serverUrl: "http://localhost:1625",
438
+ adminSecret: "admin-secret",
439
+ migrationsDir,
440
+ fromHash: fromShortHash,
441
+ toHash: toShortHash,
442
+ });
443
+ expect(fetchMock).toHaveBeenCalledTimes(2);
277
444
  });
278
- it("fails when permissions.ts has no default/permissions export", async () => {
279
- const { schemaDir, jazzBin } = await createWorkspace();
280
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
281
- await writeFile(join(schemaDir, "permissions.ts"), permissionsSchemaMissingExport());
282
- await expect(build({ schemaDir, jazzBin })).rejects.toThrow(/missing permissions export/i);
445
+ });
446
+ describe("cli permissions", () => {
447
+ it("reports the current permissions head against the matching stored structural schema", async () => {
448
+ const { root } = await createWorkspace();
449
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
450
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema());
451
+ const schemaHash = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
452
+ const fetchMock = vi.fn(async (input) => {
453
+ if (input.endsWith("/schemas")) {
454
+ return new Response(JSON.stringify({ hashes: [schemaHash] }), { status: 200 });
455
+ }
456
+ if (input.endsWith(`/schema/${schemaHash}`)) {
457
+ return new Response(JSON.stringify(storedRootSchema()), { status: 200 });
458
+ }
459
+ if (input.endsWith("/admin/permissions/head")) {
460
+ return new Response(JSON.stringify({
461
+ head: {
462
+ schemaHash,
463
+ version: 3,
464
+ parentBundleObjectId: "11111111-1111-1111-1111-111111111111",
465
+ bundleObjectId: "22222222-2222-2222-2222-222222222222",
466
+ },
467
+ }), { status: 200 });
468
+ }
469
+ throw new Error(`Unexpected fetch: ${input}`);
470
+ });
471
+ vi.stubGlobal("fetch", fetchMock);
472
+ const { logs } = await captureConsoleLogs(() => permissionsStatus({
473
+ serverUrl: "http://localhost:1625",
474
+ adminSecret: "admin-secret",
475
+ schemaDir: root,
476
+ }));
477
+ expect(logs).toContain(`Loaded structural schema from ${join(root, "schema.ts")}.`);
478
+ expect(logs).toContain(`Loaded current permissions from ${join(root, "permissions.ts")}.`);
479
+ expect(logs).toContain(`Local structural schema matches stored hash ${schemaHash.slice(0, 12)}.`);
480
+ expect(logs).toContain(`Server permissions head is v3 on ${schemaHash.slice(0, 12)}.`);
481
+ expect(logs).toContain("Next push will require parent bundle 22222222-2222-2222-2222-222222222222.");
283
482
  });
284
- it("fails when permissions.ts references unknown tables", async () => {
285
- const { schemaDir, jazzBin } = await createWorkspace();
286
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
287
- await writeFile(join(schemaDir, "permissions.ts"), permissionsSchemaUnknownTable());
288
- await expect(build({ schemaDir, jazzBin })).rejects.toThrow(/permissions\.ts defines permissions for unknown table\(s\): ghosts/i);
483
+ it("loads src/permissions.ts when reporting the current permissions head", async () => {
484
+ const { root } = await createWorkspace();
485
+ const srcDir = join(root, "src");
486
+ await mkdir(srcDir, { recursive: true });
487
+ await writeFile(join(srcDir, "schema.ts"), rootSchemaWithoutInlinePermissions());
488
+ await writeFile(join(srcDir, "permissions.ts"), rootPermissionsSchema());
489
+ const schemaHash = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
490
+ const fetchMock = vi.fn(async (input) => {
491
+ if (input.endsWith("/schemas")) {
492
+ return new Response(JSON.stringify({ hashes: [schemaHash] }), { status: 200 });
493
+ }
494
+ if (input.endsWith(`/schema/${schemaHash}`)) {
495
+ return new Response(JSON.stringify(storedRootSchema()), { status: 200 });
496
+ }
497
+ if (input.endsWith("/admin/permissions/head")) {
498
+ return new Response(JSON.stringify({ head: null }), { status: 200 });
499
+ }
500
+ throw new Error(`Unexpected fetch: ${input}`);
501
+ });
502
+ vi.stubGlobal("fetch", fetchMock);
503
+ const { logs } = await captureConsoleLogs(() => permissionsStatus({
504
+ serverUrl: "http://localhost:1625",
505
+ adminSecret: "admin-secret",
506
+ schemaDir: root,
507
+ }));
508
+ expect(logs).toContain(`Loaded structural schema from ${join(srcDir, "schema.ts")}.`);
509
+ expect(logs).toContain(`Loaded current permissions from ${join(srcDir, "permissions.ts")}.`);
289
510
  });
290
- it("accepts named permissions export for transitional ergonomics", async () => {
291
- const { schemaDir, jazzBin } = await createWorkspace();
292
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
293
- await writeFile(join(schemaDir, "permissions.ts"), permissionsSchemaNamedExport());
294
- await build({ schemaDir, jazzBin });
295
- const sql = await readFile(join(schemaDir, "current.sql"), "utf8");
296
- expect(sql).toContain("CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);");
511
+ it("publishes permissions with the current head bundle as the expected parent", async () => {
512
+ const { root } = await createWorkspace();
513
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
514
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema());
515
+ const schemaHash = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
516
+ const currentHead = {
517
+ schemaHash,
518
+ version: 2,
519
+ parentBundleObjectId: "11111111-1111-1111-1111-111111111111",
520
+ bundleObjectId: "22222222-2222-2222-2222-222222222222",
521
+ };
522
+ const fetchMock = vi.fn(async (input, init) => {
523
+ if (input.endsWith("/schemas")) {
524
+ return new Response(JSON.stringify({ hashes: [schemaHash] }), { status: 200 });
525
+ }
526
+ if (input.endsWith(`/schema/${schemaHash}`)) {
527
+ return new Response(JSON.stringify(storedRootSchema()), { status: 200 });
528
+ }
529
+ if (input.endsWith("/admin/permissions/head")) {
530
+ return new Response(JSON.stringify({ head: currentHead }), { status: 200 });
531
+ }
532
+ if (input.endsWith("/admin/permissions")) {
533
+ const body = JSON.parse(String(init?.body));
534
+ expect(body.schemaHash).toBe(schemaHash);
535
+ expect(body.expectedParentBundleObjectId).toBe(currentHead.bundleObjectId);
536
+ expect(Object.keys(body.permissions)).toContain("todos");
537
+ return new Response(JSON.stringify({
538
+ head: {
539
+ schemaHash,
540
+ version: 3,
541
+ parentBundleObjectId: currentHead.bundleObjectId,
542
+ bundleObjectId: "33333333-3333-3333-3333-333333333333",
543
+ },
544
+ }), { status: 201 });
545
+ }
546
+ throw new Error(`Unexpected fetch: ${input}`);
547
+ });
548
+ vi.stubGlobal("fetch", fetchMock);
549
+ const { logs } = await captureConsoleLogs(() => pushPermissions({
550
+ serverUrl: "http://localhost:1625",
551
+ adminSecret: "admin-secret",
552
+ schemaDir: root,
553
+ }));
554
+ expect(logs).toContain(`Resolved structural schema hash ${schemaHash.slice(0, 12)}.`);
555
+ expect(logs).toContain(`Publishing from parent v2 on ${schemaHash.slice(0, 12)}.`);
556
+ expect(logs).toContain(`Published permissions head v3 on ${schemaHash.slice(0, 12)}.`);
557
+ expect(logs).toContain("Permission-only changes do not create schema hashes or require migrations.");
297
558
  });
298
- it("fails when permissions.ts export shape is invalid", async () => {
299
- const { schemaDir, jazzBin } = await createWorkspace();
300
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithoutInlinePermissions());
301
- await writeFile(join(schemaDir, "permissions.ts"), permissionsSchemaInvalidShape());
302
- await expect(build({ schemaDir, jazzBin })).rejects.toThrow(/invalid permissions export/i);
559
+ it("publishes permission literals using tagged wire values", async () => {
560
+ const { root } = await createWorkspace();
561
+ await writeFile(join(root, "schema.ts"), rootSchemaWithBooleanTodo());
562
+ await writeFile(join(root, "permissions.ts"), rootBooleanLiteralPermissionsSchema());
563
+ const schemaHash = "abababababababababababababababababababababababababababababababab";
564
+ const fetchMock = vi.fn(async (input, init) => {
565
+ if (input.endsWith("/schemas")) {
566
+ return new Response(JSON.stringify({ hashes: [schemaHash] }), { status: 200 });
567
+ }
568
+ if (input.endsWith(`/schema/${schemaHash}`)) {
569
+ return new Response(JSON.stringify({
570
+ todos: {
571
+ columns: [
572
+ { name: "title", column_type: { type: "Text" }, nullable: false },
573
+ { name: "done", column_type: { type: "Boolean" }, nullable: false },
574
+ ],
575
+ },
576
+ }), { status: 200 });
577
+ }
578
+ if (input.endsWith("/admin/permissions/head")) {
579
+ return new Response(JSON.stringify({ head: null }), { status: 200 });
580
+ }
581
+ if (input.endsWith("/admin/permissions")) {
582
+ const body = JSON.parse(String(init?.body));
583
+ expect(body.permissions.todos.select.using).toEqual({
584
+ type: "Cmp",
585
+ column: "done",
586
+ op: "Eq",
587
+ value: {
588
+ type: "Literal",
589
+ value: {
590
+ type: "Boolean",
591
+ value: true,
592
+ },
593
+ },
594
+ });
595
+ return new Response(JSON.stringify({
596
+ head: {
597
+ schemaHash,
598
+ version: 1,
599
+ parentBundleObjectId: null,
600
+ bundleObjectId: "99999999-9999-9999-9999-999999999999",
601
+ },
602
+ }), { status: 201 });
603
+ }
604
+ throw new Error(`Unexpected fetch: ${input}`);
605
+ });
606
+ vi.stubGlobal("fetch", fetchMock);
607
+ await pushPermissions({
608
+ serverUrl: "http://localhost:1625",
609
+ adminSecret: "admin-secret",
610
+ schemaDir: root,
611
+ });
612
+ expect(fetchMock).toHaveBeenCalled();
303
613
  });
304
614
  });
305
- // Integration test: exercises the bin/jazz-tools.js entry point, which applies extra
306
- // logic on top of build() to decide whether to invoke the TS CLI at all.
307
- const binPath = fileURLToPath(new URL("../bin/jazz-tools.js", import.meta.url));
308
- function runBinBuild(schemaDir, rustBin) {
309
- const result = spawnSync(process.execPath, [binPath, "build", "--schema-dir", schemaDir, "--rust-bin", rustBin], {
310
- stdio: "inherit",
615
+ function runBin(args) {
616
+ return spawnSync(process.execPath, [binPath, ...args], {
617
+ encoding: "utf8",
618
+ env: process.env,
311
619
  });
312
- expect(result.status).toBe(0);
620
+ }
621
+ function hostNativeBinaryName() {
622
+ switch (`${process.platform}-${process.arch}`) {
623
+ case "darwin-arm64":
624
+ return "jazz-tools-darwin-arm64";
625
+ case "darwin-x64":
626
+ return "jazz-tools-darwin-x64";
627
+ case "linux-arm64":
628
+ return "jazz-tools-linux-arm64";
629
+ case "linux-x64":
630
+ return "jazz-tools-linux-x64";
631
+ default:
632
+ return null;
633
+ }
313
634
  }
314
635
  describe("bin integration", () => {
315
- it("generates current.sql on first build (no current.sql)", async () => {
316
- const { schemaDir } = await createWorkspace();
317
- const rustBin = await createFakeRustBin();
318
- await writeFile(join(schemaDir, "current.ts"), binCurrentSchema());
319
- runBinBuild(schemaDir, rustBin);
320
- await readFile(join(schemaDir, "current.sql"), "utf8");
636
+ it("routes validate through the TypeScript CLI for a root schema.ts project", async () => {
637
+ const { root } = await createWorkspace();
638
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
639
+ const result = runBin(["validate", "--schema-dir", root]);
640
+ expect(result.status).toBe(0);
641
+ expect(await fileExists(join(root, "schema", "current.sql"))).toBe(false);
642
+ expect(await fileExists(join(root, "schema", "app.ts"))).toBe(false);
643
+ });
644
+ it("loads root permissions.ts through the validate command", async () => {
645
+ const { root } = await createWorkspace();
646
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
647
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema("./schema.ts", distIndexPath));
648
+ const result = runBin(["validate", "--schema-dir", root]);
649
+ expect(result.status).toBe(0);
650
+ expect(await fileExists(join(root, "permissions.test.ts"))).toBe(false);
651
+ });
652
+ it("loads src/schema.ts and src/permissions.ts through the validate command", async () => {
653
+ const { root } = await createWorkspace();
654
+ const srcDir = join(root, "src");
655
+ await mkdir(srcDir, { recursive: true });
656
+ await writeFile(join(srcDir, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
657
+ await writeFile(join(srcDir, "permissions.ts"), rootPermissionsSchema("./schema.ts", distIndexPath));
658
+ const result = runBin(["validate", "--schema-dir", root]);
659
+ expect(result.status).toBe(0);
660
+ expect(result.stdout).toContain(`Loaded structural schema from ${join(srcDir, "schema.ts")}.`);
661
+ expect(result.stdout).toContain(`Loaded current permissions from ${join(srcDir, "permissions.ts")}.`);
321
662
  });
322
- it("generates app.ts on first build (no current.sql)", async () => {
323
- const { schemaDir } = await createWorkspace();
324
- const rustBin = await createFakeRustBin();
325
- await writeFile(join(schemaDir, "current.ts"), binCurrentSchema());
326
- runBinBuild(schemaDir, rustBin);
327
- await readFile(join(schemaDir, "app.ts"), "utf8");
663
+ it("fails when validate is pointed at the legacy ./schema shim directory", async () => {
664
+ const { root, schemaDir } = await createWorkspace();
665
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
666
+ const result = runBin(["validate", "--schema-dir", schemaDir]);
667
+ expect(result.status).toBe(1);
668
+ expect(result.stderr).toContain("Schema file not found");
328
669
  });
329
- it("regenerates current.sql and app.ts when current.sql already exists", async () => {
330
- // Regression: bin skips the TS CLI step when current.sql is present,
331
- // so neither file is updated on subsequent builds.
332
- const { schemaDir } = await createWorkspace();
333
- const rustBin = await createFakeRustBin();
334
- await writeFile(join(schemaDir, "current.ts"), binCurrentSchema());
335
- await writeFile(join(schemaDir, "current.sql"), "-- stale");
336
- runBinBuild(schemaDir, rustBin);
337
- const sql = await readFile(join(schemaDir, "current.sql"), "utf8");
338
- expect(sql).toContain("CREATE TABLE");
339
- await readFile(join(schemaDir, "app.ts"), "utf8");
670
+ it("fails when no root schema.ts can be found", async () => {
671
+ const { root } = await createWorkspace();
672
+ const result = runBin(["validate", "--schema-dir", root]);
673
+ expect(result.status).toBe(1);
674
+ expect(result.stderr).toContain("Schema file not found");
340
675
  });
341
- // bootstrap change current.ts rebuild
342
- it("updates current.sql when current.ts changes after initial build", async () => {
343
- const { schemaDir } = await createWorkspace();
344
- const rustBin = await createFakeRustBin();
345
- await writeFile(join(schemaDir, "current.ts"), binCurrentSchema());
346
- runBinBuild(schemaDir, rustBin);
347
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithComments());
348
- runBinBuild(schemaDir, rustBin);
349
- const sql = await readFile(join(schemaDir, "current.sql"), "utf8");
350
- expect(sql).toContain("CREATE TABLE comments");
676
+ it("rejects the removed build alias with a validate hint", async () => {
677
+ const { root } = await createWorkspace();
678
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
679
+ const result = runBin(["build", "--schema-dir", root]);
680
+ expect(result.status).toBe(1);
681
+ expect(result.stderr).toContain("renamed to `jazz-tools validate`");
351
682
  });
352
- it("updates app.ts when current.ts changes after initial build", async () => {
353
- const { schemaDir } = await createWorkspace();
354
- const rustBin = await createFakeRustBin();
355
- await writeFile(join(schemaDir, "current.ts"), binCurrentSchema());
356
- runBinBuild(schemaDir, rustBin);
357
- await writeFile(join(schemaDir, "current.ts"), currentSchemaWithComments());
358
- runBinBuild(schemaDir, rustBin);
359
- const appTs = await readFile(join(schemaDir, "app.ts"), "utf8");
360
- expect(appTs).toContain("comments");
683
+ it("routes schema export through the TypeScript CLI", async () => {
684
+ const { root } = await createWorkspace();
685
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
686
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema("./schema.ts", distIndexPath));
687
+ const result = runBin(["schema", "export", "--schema-dir", root, "--format", "json"]);
688
+ expect(result.status).toBe(0);
689
+ const exported = JSON.parse(String(result.stdout));
690
+ expect(exported.todos.columns.some((column) => column.name === "ownerId")).toBe(true);
361
691
  });
362
- it("generates migration SQL from stub on rebuild when current.sql already exists", async () => {
363
- const { schemaDir } = await createWorkspace();
364
- const rustBin = await createFakeRustBin();
365
- await writeFile(join(schemaDir, "current.ts"), binSchemaWithMessagesAndCanvases());
366
- runBinBuild(schemaDir, rustBin);
367
- await writeFile(join(schemaDir, "migration_v1_v2_aaaaaaaaaaaa_bbbbbbbbbbbb.ts"), binMigrationDropIsPublicFromBothTables());
368
- runBinBuild(schemaDir, rustBin);
369
- await readFile(join(schemaDir, "migration_v1_v2_fwd_aaaaaaaaaaaa_bbbbbbbbbbbb.sql"), "utf8");
370
- await readFile(join(schemaDir, "migration_v1_v2_bwd_aaaaaaaaaaaa_bbbbbbbbbbbb.sql"), "utf8");
692
+ it("verifies packed runtime bootstrap with a native-only help probe", async () => {
693
+ const hostBinaryName = hostNativeBinaryName();
694
+ if (!hostBinaryName) {
695
+ return;
696
+ }
697
+ const { root } = await createWorkspace();
698
+ const packageRoot = join(root, "package");
699
+ const nativeDir = join(packageRoot, "bin", "native");
700
+ const argsPath = join(root, "captured-args.txt");
701
+ const binaryPath = join(nativeDir, hostBinaryName);
702
+ await mkdir(nativeDir, { recursive: true });
703
+ await copyFile(binPath, join(packageRoot, "bin", "jazz-tools.js"));
704
+ await writeFile(binaryPath, `#!/bin/sh
705
+ printf '%s\n' "$@" > ${JSON.stringify(argsPath)}
706
+ exit 0
707
+ `, "utf8");
708
+ await chmod(binaryPath, 0o644);
709
+ const result = spawnSync(process.execPath, [bootstrapVerifierPath, packageRoot], {
710
+ encoding: "utf8",
711
+ env: process.env,
712
+ });
713
+ expect(result.status).toBe(0);
714
+ expect(await readFile(argsPath, "utf8")).toBe("create\n--help\n");
715
+ await expect(access(binaryPath, constants.X_OK)).resolves.toBeUndefined();
371
716
  });
372
- it("loads permissions.ts that imports ./app via bin entry point", async () => {
373
- const { schemaDir } = await createWorkspace();
374
- const rustBin = await createFakeRustBin();
375
- await writeFile(join(schemaDir, "current.ts"), binCurrentSchema());
376
- await writeFile(join(schemaDir, "permissions.ts"), binPermissionsSchema("./app"));
377
- runBinBuild(schemaDir, rustBin);
378
- const sql = await readFile(join(schemaDir, "current.sql"), "utf8");
379
- expect(sql).toContain("CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);");
717
+ it("shows the wrapper command surface in --help output", () => {
718
+ const result = runBin(["--help"]);
719
+ expect(result.status).toBe(0);
720
+ expect(result.stdout).toContain("validate");
721
+ expect(result.stdout).toContain("schema export");
722
+ expect(result.stdout).toContain("permissions push");
723
+ expect(result.stdout).toContain("migrations push");
724
+ expect(result.stdout).toContain("server");
725
+ expect(result.stdout).toContain("create");
380
726
  });
381
727
  });
382
728
  //# sourceMappingURL=cli.test.js.map