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

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 (225) hide show
  1. package/bin/docs-index.db +0 -0
  2. package/bin/docs-index.txt +1624 -542
  3. package/bin/jazz-tools.js +19 -40
  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/bin/native/jazz-tools-windows-x64.exe +0 -0
  9. package/dist/backend/create-jazz-context.d.ts +31 -6
  10. package/dist/backend/create-jazz-context.d.ts.map +1 -1
  11. package/dist/backend/create-jazz-context.js +35 -5
  12. package/dist/backend/create-jazz-context.js.map +1 -1
  13. package/dist/backend/create-jazz-context.test.js +61 -6
  14. package/dist/backend/create-jazz-context.test.js.map +1 -1
  15. package/dist/cli.d.ts +29 -2
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +648 -246
  18. package/dist/cli.js.map +1 -1
  19. package/dist/cli.test.js +512 -297
  20. package/dist/cli.test.js.map +1 -1
  21. package/dist/codegen/schema-reader.d.ts.map +1 -1
  22. package/dist/codegen/schema-reader.js +6 -1
  23. package/dist/codegen/schema-reader.js.map +1 -1
  24. package/dist/dev-tools/dev-tools.d.ts.map +1 -1
  25. package/dist/dev-tools/dev-tools.js +61 -13
  26. package/dist/dev-tools/dev-tools.js.map +1 -1
  27. package/dist/dev-tools/dev-tools.test.js +166 -0
  28. package/dist/dev-tools/dev-tools.test.js.map +1 -1
  29. package/dist/dev-tools/extension-panel.d.ts.map +1 -1
  30. package/dist/dev-tools/extension-panel.js +30 -7
  31. package/dist/dev-tools/extension-panel.js.map +1 -1
  32. package/dist/dev-tools/protocol.d.ts +49 -1
  33. package/dist/dev-tools/protocol.d.ts.map +1 -1
  34. package/dist/dev-tools/protocol.js +3 -0
  35. package/dist/dev-tools/protocol.js.map +1 -1
  36. package/dist/drivers/index.d.ts +1 -1
  37. package/dist/drivers/index.d.ts.map +1 -1
  38. package/dist/drivers/schema-wire.d.ts.map +1 -1
  39. package/dist/drivers/schema-wire.js +12 -1
  40. package/dist/drivers/schema-wire.js.map +1 -1
  41. package/dist/drivers/schema-wire.test.d.ts +2 -0
  42. package/dist/drivers/schema-wire.test.d.ts.map +1 -0
  43. package/dist/drivers/schema-wire.test.js +31 -0
  44. package/dist/drivers/schema-wire.test.js.map +1 -0
  45. package/dist/drivers/types.d.ts +2 -0
  46. package/dist/drivers/types.d.ts.map +1 -1
  47. package/dist/dsl.d.ts +139 -95
  48. package/dist/dsl.d.ts.map +1 -1
  49. package/dist/dsl.js +64 -8
  50. package/dist/dsl.js.map +1 -1
  51. package/dist/dsl.test.js +78 -8
  52. package/dist/dsl.test.js.map +1 -1
  53. package/dist/index.d.ts +32 -3
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +16 -3
  56. package/dist/index.js.map +1 -1
  57. package/dist/magic-columns.d.ts +3 -1
  58. package/dist/magic-columns.d.ts.map +1 -1
  59. package/dist/magic-columns.js +20 -4
  60. package/dist/magic-columns.js.map +1 -1
  61. package/dist/mcp/build-index.test.js +1 -1
  62. package/dist/migrations.d.ts +126 -0
  63. package/dist/migrations.d.ts.map +1 -0
  64. package/dist/migrations.js +112 -0
  65. package/dist/migrations.js.map +1 -0
  66. package/dist/permissions/index.test.js +35 -0
  67. package/dist/permissions/index.test.js.map +1 -1
  68. package/dist/react-native/create-jazz-client.test.js +62 -42
  69. package/dist/react-native/create-jazz-client.test.js.map +1 -1
  70. package/dist/react-native/jazz-rn-runtime-adapter.d.ts +18 -3
  71. package/dist/react-native/jazz-rn-runtime-adapter.d.ts.map +1 -1
  72. package/dist/react-native/jazz-rn-runtime-adapter.js +110 -6
  73. package/dist/react-native/jazz-rn-runtime-adapter.js.map +1 -1
  74. package/dist/react-native/jazz-rn-runtime-adapter.test.js +149 -4
  75. package/dist/react-native/jazz-rn-runtime-adapter.test.js.map +1 -1
  76. package/dist/reconcile-array.d.ts +29 -0
  77. package/dist/reconcile-array.d.ts.map +1 -0
  78. package/dist/reconcile-array.js +110 -0
  79. package/dist/reconcile-array.js.map +1 -0
  80. package/dist/reconcile-array.test.d.ts +2 -0
  81. package/dist/reconcile-array.test.d.ts.map +1 -0
  82. package/dist/reconcile-array.test.js +118 -0
  83. package/dist/reconcile-array.test.js.map +1 -0
  84. package/dist/runtime/client.d.ts +24 -20
  85. package/dist/runtime/client.d.ts.map +1 -1
  86. package/dist/runtime/client.for-request.test.js +8 -8
  87. package/dist/runtime/client.for-request.test.js.map +1 -1
  88. package/dist/runtime/client.js +58 -25
  89. package/dist/runtime/client.js.map +1 -1
  90. package/dist/runtime/client.mutations.test.js +72 -1
  91. package/dist/runtime/client.mutations.test.js.map +1 -1
  92. package/dist/runtime/cloud-server.integration.test.js +145 -88
  93. package/dist/runtime/cloud-server.integration.test.js.map +1 -1
  94. package/dist/runtime/db.d.ts +3 -7
  95. package/dist/runtime/db.d.ts.map +1 -1
  96. package/dist/runtime/db.js +16 -14
  97. package/dist/runtime/db.js.map +1 -1
  98. package/dist/runtime/db.schema-order.test.js +8 -8
  99. package/dist/runtime/db.schema-order.test.js.map +1 -1
  100. package/dist/runtime/index.d.ts +1 -1
  101. package/dist/runtime/index.d.ts.map +1 -1
  102. package/dist/runtime/index.js +1 -1
  103. package/dist/runtime/index.js.map +1 -1
  104. package/dist/runtime/napi.integration.test.js +113 -136
  105. package/dist/runtime/napi.integration.test.js.map +1 -1
  106. package/dist/runtime/query-adapter.d.ts.map +1 -1
  107. package/dist/runtime/query-adapter.js +22 -2
  108. package/dist/runtime/query-adapter.js.map +1 -1
  109. package/dist/runtime/query-adapter.test.js +81 -5
  110. package/dist/runtime/query-adapter.test.js.map +1 -1
  111. package/dist/runtime/row-transformer.js +2 -2
  112. package/dist/runtime/row-transformer.js.map +1 -1
  113. package/dist/runtime/row-transformer.test.js +9 -9
  114. package/dist/runtime/row-transformer.test.js.map +1 -1
  115. package/dist/runtime/schema-fetch.d.ts +103 -1
  116. package/dist/runtime/schema-fetch.d.ts.map +1 -1
  117. package/dist/runtime/schema-fetch.js +106 -0
  118. package/dist/runtime/schema-fetch.js.map +1 -1
  119. package/dist/runtime/sync-transport.d.ts.map +1 -1
  120. package/dist/runtime/sync-transport.js +15 -0
  121. package/dist/runtime/sync-transport.js.map +1 -1
  122. package/dist/runtime/sync-transport.test.js +33 -0
  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 +217 -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 +15 -13
  177. package/dist/codegen/codegen.test.d.ts +0 -2
  178. package/dist/codegen/codegen.test.d.ts.map +0 -1
  179. package/dist/codegen/codegen.test.js +0 -1134
  180. package/dist/codegen/codegen.test.js.map +0 -1
  181. package/dist/codegen/index.d.ts +0 -18
  182. package/dist/codegen/index.d.ts.map +0 -1
  183. package/dist/codegen/index.js +0 -22
  184. package/dist/codegen/index.js.map +0 -1
  185. package/dist/codegen/query-builder-generator.d.ts +0 -26
  186. package/dist/codegen/query-builder-generator.d.ts.map +0 -1
  187. package/dist/codegen/query-builder-generator.js +0 -377
  188. package/dist/codegen/query-builder-generator.js.map +0 -1
  189. package/dist/codegen/type-generator.d.ts +0 -30
  190. package/dist/codegen/type-generator.d.ts.map +0 -1
  191. package/dist/codegen/type-generator.js +0 -368
  192. package/dist/codegen/type-generator.js.map +0 -1
  193. package/dist/runtime/napi.fjall.db.all.integration.test.d.ts +0 -2
  194. package/dist/runtime/napi.fjall.db.all.integration.test.d.ts.map +0 -1
  195. package/dist/runtime/napi.fjall.db.all.integration.test.js +0 -76
  196. package/dist/runtime/napi.fjall.db.all.integration.test.js.map +0 -1
  197. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts +0 -2
  198. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts.map +0 -1
  199. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js +0 -47
  200. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js.map +0 -1
  201. package/dist/runtime/napi.fjall.test-helpers.d.ts +0 -34
  202. package/dist/runtime/napi.fjall.test-helpers.d.ts.map +0 -1
  203. package/dist/runtime/napi.fjall.test-helpers.js +0 -172
  204. package/dist/runtime/napi.fjall.test-helpers.js.map +0 -1
  205. package/dist/sql-gen.d.ts +0 -5
  206. package/dist/sql-gen.d.ts.map +0 -1
  207. package/dist/sql-gen.js +0 -234
  208. package/dist/sql-gen.js.map +0 -1
  209. package/dist/sql-gen.test.d.ts +0 -2
  210. package/dist/sql-gen.test.d.ts.map +0 -1
  211. package/dist/sql-gen.test.js +0 -481
  212. package/dist/sql-gen.test.js.map +0 -1
  213. package/dist/svelte/context.test.d.ts +0 -2
  214. package/dist/svelte/context.test.d.ts.map +0 -1
  215. package/dist/svelte/context.test.js +0 -55
  216. package/dist/svelte/use-all.test.d.ts +0 -2
  217. package/dist/svelte/use-all.test.d.ts.map +0 -1
  218. package/dist/svelte/use-all.test.js +0 -147
  219. package/dist/testing/fixtures/basic/app.d.ts +0 -59
  220. package/dist/testing/fixtures/basic/app.d.ts.map +0 -1
  221. package/dist/testing/fixtures/basic/app.js.map +0 -1
  222. package/dist/testing/fixtures/basic/current.d.ts +0 -2
  223. package/dist/testing/fixtures/basic/current.d.ts.map +0 -1
  224. package/dist/testing/fixtures/basic/current.js +0 -6
  225. package/dist/testing/fixtures/basic/current.js.map +0 -1
package/dist/cli.test.js CHANGED
@@ -1,18 +1,17 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { chmod, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { afterEach, describe, expect, it } from "vitest";
7
- import { build } from "./cli.js";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { createMigration, exportSchema, permissionsStatus, pushMigration, pushPermissions, validate, } from "./cli.js";
8
8
  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));
9
+ const indexPath = fileURLToPath(new URL("./index.ts", import.meta.url));
10
+ const distIndexPath = fileURLToPath(new URL("../dist/index.js", import.meta.url));
11
+ const binPath = fileURLToPath(new URL("../bin/jazz-tools.js", import.meta.url));
14
12
  const tempRoots = [];
15
13
  afterEach(async () => {
14
+ vi.unstubAllGlobals();
16
15
  await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
17
16
  });
18
17
  async function createWorkspace() {
@@ -20,44 +19,72 @@ async function createWorkspace() {
20
19
  tempRoots.push(root);
21
20
  const schemaDir = join(root, "schema");
22
21
  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 };
22
+ await writeFile(join(root, "package.json"), '{ "type": "module" }\n');
23
+ return { root, schemaDir };
27
24
  }
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;
25
+ async function fileExists(path) {
26
+ try {
27
+ await access(path);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ async function captureConsoleLogs(run) {
35
+ const logs = [];
36
+ const spy = vi
37
+ .spyOn(console, "log")
38
+ .mockImplementation((message, ...rest) => {
39
+ logs.push([message, ...rest].map((value) => String(value ?? "")).join(" "));
40
+ });
41
+ try {
42
+ const result = await run();
43
+ return { result, logs };
44
+ }
45
+ finally {
46
+ spy.mockRestore();
47
+ }
35
48
  }
36
- function currentSchemaWithoutInlinePermissions() {
49
+ function rootSchemaWithoutInlinePermissions(indexImportPath = indexPath) {
37
50
  return `
38
- import { table, col } from ${JSON.stringify(dslPath)};
51
+ import { schema as s } from ${JSON.stringify(indexImportPath)};
39
52
 
40
- table("projects", {
41
- name: col.string(),
42
- });
53
+ const schema = {
54
+ projects: s.table({
55
+ name: s.string(),
56
+ }),
57
+ todos: s.table({
58
+ title: s.string(),
59
+ ownerId: s.string(),
60
+ }),
61
+ };
43
62
 
44
- table("todos", {
45
- title: col.string(),
46
- ownerId: col.string(),
47
- });
63
+ type AppSchema = s.Schema<typeof schema>;
64
+ export const app: s.App<AppSchema> = s.defineApp(schema);
48
65
  `;
49
66
  }
50
- function currentSchemaWithInlinePermissions() {
67
+ function rootSchemaWithBooleanTodo(indexImportPath = indexPath) {
51
68
  return `
52
- import { table, col } from ${JSON.stringify(dslPath)};
69
+ import { schema as s } from ${JSON.stringify(indexImportPath)};
53
70
 
54
- table("projects", {
55
- name: col.string(),
56
- });
71
+ const schema = {
72
+ todos: s.table({
73
+ title: s.string(),
74
+ done: s.boolean(),
75
+ }),
76
+ };
77
+
78
+ type AppSchema = s.Schema<typeof schema>;
79
+ export const app: s.App<AppSchema> = s.defineApp(schema);
80
+ `;
81
+ }
82
+ function rootSchemaWithInlinePermissions(dslImportPath = dslPath) {
83
+ return `
84
+ import { table, col } from ${JSON.stringify(dslImportPath)};
57
85
 
58
86
  table("todos", {
59
87
  title: col.string(),
60
- ownerId: col.string(),
61
88
  }, {
62
89
  permissions: {
63
90
  select: { type: "True" },
@@ -65,16 +92,31 @@ table("todos", {
65
92
  });
66
93
  `;
67
94
  }
68
- function permissionsSchema(appImportPath = "./app.js") {
95
+ function rootPermissionsSchema(appImportPath = "./schema.ts", importPath = indexPath) {
96
+ return `
97
+ import { schema as s } from ${JSON.stringify(importPath)};
98
+ import { app } from ${JSON.stringify(appImportPath)};
99
+
100
+ export default s.definePermissions(app, ({ policy, session }) => [
101
+ policy.todos.allowRead.where({ ownerId: session.user_id }),
102
+ ]);
103
+ `;
104
+ }
105
+ function rootBooleanLiteralPermissionsSchema(appImportPath = "./schema.ts", importPath = indexPath) {
69
106
  return `
70
- import { definePermissions } from ${JSON.stringify(permissionsDslPath)};
107
+ import { schema as s } from ${JSON.stringify(importPath)};
71
108
  import { app } from ${JSON.stringify(appImportPath)};
72
109
 
73
- export default definePermissions(app, ({ policy, session }) => [
74
- policy.todos.allowRead.where({ owner_id: session.user_id }),
110
+ export default s.definePermissions(app, ({ policy }) => [
111
+ policy.todos.allowRead.where({ done: true }),
75
112
  ]);
76
113
  `;
77
114
  }
115
+ function permissionsSchemaMissingExport() {
116
+ return `
117
+ export const nope = 42;
118
+ `;
119
+ }
78
120
  function permissionsSchemaUnknownTable() {
79
121
  return `
80
122
  export default {
@@ -86,18 +128,13 @@ export default {
86
128
  };
87
129
  `;
88
130
  }
89
- function permissionsSchemaMissingExport() {
90
- return `
91
- export const nope = 42;
92
- `;
93
- }
94
- function permissionsSchemaNamedExport() {
131
+ function permissionsSchemaNamedExport(appImportPath = "./schema.ts", importPath = indexPath) {
95
132
  return `
96
- import { definePermissions } from ${JSON.stringify(permissionsDslPath)};
97
- import { app } from "./app.js";
133
+ import { schema as s } from ${JSON.stringify(importPath)};
134
+ import { app } from ${JSON.stringify(appImportPath)};
98
135
 
99
- export const permissions = definePermissions(app, ({ policy, session }) => [
100
- policy.todos.allowRead.where({ owner_id: session.user_id }),
136
+ export const permissions = s.definePermissions(app, ({ policy, session }) => [
137
+ policy.todos.allowRead.where({ ownerId: session.user_id }),
101
138
  ]);
102
139
  `;
103
140
  }
@@ -108,275 +145,453 @@ export default {
108
145
  };
109
146
  `;
110
147
  }
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
- `;
125
- }
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
- `;
148
+ function storedRootSchema() {
149
+ return {
150
+ projects: {
151
+ columns: [{ name: "name", column_type: { type: "Text" }, nullable: false }],
152
+ },
153
+ todos: {
154
+ columns: [
155
+ { name: "title", column_type: { type: "Text" }, nullable: false },
156
+ { name: "ownerId", column_type: { type: "Text" }, nullable: false },
157
+ ],
158
+ },
159
+ };
153
160
  }
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");
161
+ describe("cli validate", () => {
162
+ it("validates root schema.ts without generating SQL or app artifacts", async () => {
163
+ const { root } = await createWorkspace();
164
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
165
+ await validate({ schemaDir: root });
166
+ expect(await fileExists(join(root, "schema", "current.sql"))).toBe(false);
167
+ expect(await fileExists(join(root, "schema", "app.ts"))).toBe(false);
168
+ expect(await fileExists(join(root, "permissions.test.ts"))).toBe(false);
217
169
  });
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;");
170
+ it("finds root schema.ts when pointed at the default ./schema shim directory", async () => {
171
+ const { root, schemaDir } = await createWorkspace();
172
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
173
+ await validate({ schemaDir });
174
+ expect(await fileExists(join(schemaDir, "current.sql"))).toBe(false);
175
+ expect(await fileExists(join(schemaDir, "app.ts"))).toBe(false);
231
176
  });
232
- });
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.");
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.");
247
187
  });
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);");
188
+ it("accepts named permissions exports for transitional ergonomics", async () => {
189
+ const { root } = await createWorkspace();
190
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
191
+ await writeFile(join(root, "permissions.ts"), permissionsSchemaNamedExport());
192
+ await validate({ schemaDir: root });
255
193
  });
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);");
194
+ it("fails when schema.ts uses inline table permissions", async () => {
195
+ const { root } = await createWorkspace();
196
+ await writeFile(join(root, "schema.ts"), rootSchemaWithInlinePermissions());
197
+ await expect(validate({ schemaDir: root })).rejects.toThrow(/inline table permissions are no longer supported/i);
263
198
  });
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);
199
+ it("fails when permissions.ts has no default or named permissions export", async () => {
200
+ const { root } = await createWorkspace();
201
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
202
+ await writeFile(join(root, "permissions.ts"), permissionsSchemaMissingExport());
203
+ await expect(validate({ schemaDir: root })).rejects.toThrow(/missing permissions export/i);
268
204
  });
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");
205
+ it("fails when permissions.ts references unknown tables", async () => {
206
+ const { root } = await createWorkspace();
207
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
208
+ await writeFile(join(root, "permissions.ts"), permissionsSchemaUnknownTable());
209
+ await expect(validate({ schemaDir: root })).rejects.toThrow(/permissions\.ts defines permissions for unknown table\(s\): ghosts/i);
277
210
  });
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);
211
+ it("fails when permissions.ts export shape is invalid", async () => {
212
+ const { root } = await createWorkspace();
213
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
214
+ await writeFile(join(root, "permissions.ts"), permissionsSchemaInvalidShape());
215
+ await expect(validate({ schemaDir: root })).rejects.toThrow(/invalid permissions export/i);
283
216
  });
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);
217
+ });
218
+ describe("cli schema export", () => {
219
+ it("prints the compiled schema representation as JSON", async () => {
220
+ const { root } = await createWorkspace();
221
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
222
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema());
223
+ const writes = [];
224
+ const originalWrite = process.stdout.write.bind(process.stdout);
225
+ const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk) => {
226
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
227
+ return true;
228
+ }));
229
+ try {
230
+ await exportSchema({ schemaDir: root, format: "json" });
231
+ }
232
+ finally {
233
+ writeSpy.mockRestore();
234
+ process.stdout.write = originalWrite;
235
+ }
236
+ const exported = JSON.parse(writes.join(""));
237
+ expect(exported.projects.columns[0].name).toBe("name");
238
+ expect(exported.todos.columns.map((column) => column.name)).toEqual([
239
+ "title",
240
+ "ownerId",
241
+ ]);
242
+ expect(exported.todos.policies).toBeUndefined();
289
243
  });
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);");
244
+ });
245
+ describe("cli migrations", () => {
246
+ it("generates a typed migration stub from stored schema hashes", async () => {
247
+ const { root } = await createWorkspace();
248
+ const migrationsDir = join(root, "migrations");
249
+ const fromHash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
250
+ const toHash = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
251
+ const fromShortHash = fromHash.slice(0, 12);
252
+ const toShortHash = toHash.slice(0, 12);
253
+ const fetchMock = vi.fn(async (input) => {
254
+ if (input.endsWith("/schemas")) {
255
+ return new Response(JSON.stringify({ hashes: [fromHash, toHash] }), { status: 200 });
256
+ }
257
+ if (input.endsWith(`/schema/${fromHash}`)) {
258
+ return new Response(JSON.stringify({
259
+ todos: {
260
+ columns: [{ name: "title", column_type: { type: "Text" }, nullable: false }],
261
+ },
262
+ }), { status: 200 });
263
+ }
264
+ if (input.endsWith(`/schema/${toHash}`)) {
265
+ return new Response(JSON.stringify({
266
+ todos: {
267
+ columns: [
268
+ { name: "title", column_type: { type: "Text" }, nullable: false },
269
+ { name: "notes", column_type: { type: "Text" }, nullable: true },
270
+ ],
271
+ },
272
+ }), { status: 200 });
273
+ }
274
+ throw new Error(`Unexpected fetch: ${input}`);
275
+ });
276
+ vi.stubGlobal("fetch", fetchMock);
277
+ const { result: filePath, logs } = await captureConsoleLogs(() => createMigration({
278
+ serverUrl: "http://localhost:1625",
279
+ adminSecret: "admin-secret",
280
+ migrationsDir,
281
+ fromHash: fromShortHash,
282
+ toHash: toShortHash,
283
+ }));
284
+ const generated = await readFile(filePath, "utf8");
285
+ expect(filePath).toContain(`-unnamed-${fromShortHash}-${toShortHash}.ts`);
286
+ expect(generated).toContain("s.defineMigration");
287
+ expect(generated).toContain(`fromHash: "${fromShortHash}"`);
288
+ expect(generated).toContain(`toHash: "${toShortHash}"`);
289
+ expect(generated).toContain("migrate: {");
290
+ expect(generated).toContain('"notes": s.add.string({ default: null }),');
291
+ expect(logs).toContain("Migration stubs are only for structural schema changes.");
292
+ expect(logs).toContain("Permission-only changes do not create schema hashes or require migrations.");
297
293
  });
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);
294
+ it("skips table add/drop steps when inferring a migration stub", async () => {
295
+ const { root } = await createWorkspace();
296
+ const migrationsDir = join(root, "migrations");
297
+ const fromHash = "abababababababababababababababababababababababababababababababab";
298
+ const toHash = "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd";
299
+ const fromShortHash = fromHash.slice(0, 12);
300
+ const toShortHash = toHash.slice(0, 12);
301
+ const fetchMock = vi.fn(async (input) => {
302
+ if (input.endsWith("/schemas")) {
303
+ return new Response(JSON.stringify({ hashes: [fromHash, toHash] }), { status: 200 });
304
+ }
305
+ if (input.endsWith(`/schema/${fromHash}`)) {
306
+ return new Response(JSON.stringify({
307
+ todos: {
308
+ columns: [{ name: "title", column_type: { type: "Text" }, nullable: false }],
309
+ },
310
+ legacy_users: {
311
+ columns: [{ name: "email", column_type: { type: "Text" }, nullable: false }],
312
+ },
313
+ }), { status: 200 });
314
+ }
315
+ if (input.endsWith(`/schema/${toHash}`)) {
316
+ return new Response(JSON.stringify({
317
+ todos: {
318
+ columns: [
319
+ { name: "title", column_type: { type: "Text" }, nullable: false },
320
+ { name: "notes", column_type: { type: "Text" }, nullable: true },
321
+ ],
322
+ },
323
+ users: {
324
+ columns: [{ name: "name", column_type: { type: "Text" }, nullable: false }],
325
+ },
326
+ }), { status: 200 });
327
+ }
328
+ throw new Error(`Unexpected fetch: ${input}`);
329
+ });
330
+ vi.stubGlobal("fetch", fetchMock);
331
+ const filePath = await createMigration({
332
+ serverUrl: "http://localhost:1625",
333
+ adminSecret: "admin-secret",
334
+ migrationsDir,
335
+ fromHash: fromShortHash,
336
+ toHash: toShortHash,
337
+ });
338
+ const generated = await readFile(filePath, "utf8");
339
+ expect(generated).toContain('"todos": {');
340
+ expect(generated).toContain('"notes": s.add.string({ default: null }),');
341
+ expect(generated).not.toContain("createTable");
342
+ expect(generated).not.toContain("dropTable");
343
+ expect(generated).not.toContain('"legacy_users"');
344
+ expect(generated).not.toContain('"users"');
345
+ });
346
+ it("pushes a reviewed migration via the admin migrations endpoint", async () => {
347
+ const { root } = await createWorkspace();
348
+ const migrationsDir = join(root, "migrations");
349
+ await mkdir(migrationsDir, { recursive: true });
350
+ const fromHash = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
351
+ const toHash = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
352
+ const fromShortHash = fromHash.slice(0, 12);
353
+ const toShortHash = toHash.slice(0, 12);
354
+ const migrationPath = join(migrationsDir, `20260318-rename-${fromShortHash}-${toShortHash}.ts`);
355
+ await writeFile(migrationPath, `
356
+ import { schema as s } from ${JSON.stringify(indexPath)};
357
+
358
+ export default s.defineMigration({
359
+ migrate: {
360
+ users: {
361
+ email_address: s.renameFrom("email"),
362
+ },
363
+ },
364
+ fromHash: ${JSON.stringify(fromShortHash)},
365
+ toHash: ${JSON.stringify(toShortHash)},
366
+ from: {
367
+ users: s.table({
368
+ email: s.string(),
369
+ }),
370
+ },
371
+ to: {
372
+ users: s.table({
373
+ email_address: s.string(),
374
+ }),
375
+ },
376
+ });
377
+ `);
378
+ const fetchMock = vi.fn(async (_input, init) => {
379
+ if (_input.endsWith("/schemas")) {
380
+ return new Response(JSON.stringify({ hashes: [fromHash, toHash] }), { status: 200 });
381
+ }
382
+ const body = JSON.parse(String(init?.body));
383
+ expect(body.fromHash).toBe(fromHash);
384
+ expect(body.toHash).toBe(toHash);
385
+ expect(body.forward).toEqual([
386
+ {
387
+ table: "users",
388
+ operations: [
389
+ {
390
+ type: "rename",
391
+ column: "email",
392
+ value: "email_address",
393
+ },
394
+ ],
395
+ },
396
+ ]);
397
+ return new Response(JSON.stringify({ ok: true }), { status: 201 });
398
+ });
399
+ vi.stubGlobal("fetch", fetchMock);
400
+ await pushMigration({
401
+ serverUrl: "http://localhost:1625",
402
+ adminSecret: "admin-secret",
403
+ migrationsDir,
404
+ fromHash: fromShortHash,
405
+ toHash: toShortHash,
406
+ });
407
+ expect(fetchMock).toHaveBeenCalledTimes(2);
303
408
  });
304
409
  });
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",
410
+ describe("cli permissions", () => {
411
+ it("reports the current permissions head against the matching stored structural schema", async () => {
412
+ const { root } = await createWorkspace();
413
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions());
414
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema());
415
+ const schemaHash = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
416
+ const fetchMock = vi.fn(async (input) => {
417
+ if (input.endsWith("/schemas")) {
418
+ return new Response(JSON.stringify({ hashes: [schemaHash] }), { status: 200 });
419
+ }
420
+ if (input.endsWith(`/schema/${schemaHash}`)) {
421
+ return new Response(JSON.stringify(storedRootSchema()), { status: 200 });
422
+ }
423
+ if (input.endsWith("/admin/permissions/head")) {
424
+ return new Response(JSON.stringify({
425
+ head: {
426
+ schemaHash,
427
+ version: 3,
428
+ parentBundleObjectId: "11111111-1111-1111-1111-111111111111",
429
+ bundleObjectId: "22222222-2222-2222-2222-222222222222",
430
+ },
431
+ }), { status: 200 });
432
+ }
433
+ throw new Error(`Unexpected fetch: ${input}`);
434
+ });
435
+ vi.stubGlobal("fetch", fetchMock);
436
+ const { logs } = await captureConsoleLogs(() => permissionsStatus({
437
+ serverUrl: "http://localhost:1625",
438
+ adminSecret: "admin-secret",
439
+ schemaDir: root,
440
+ }));
441
+ expect(logs).toContain(`Loaded structural schema from ${join(root, "schema.ts")}.`);
442
+ expect(logs).toContain(`Loaded current permissions from ${join(root, "permissions.ts")}.`);
443
+ expect(logs).toContain(`Local structural schema matches stored hash ${schemaHash.slice(0, 12)}.`);
444
+ expect(logs).toContain(`Server permissions head is v3 on ${schemaHash.slice(0, 12)}.`);
445
+ expect(logs).toContain("Next push will require parent bundle 22222222-2222-2222-2222-222222222222.");
311
446
  });
312
- expect(result.status).toBe(0);
313
- }
314
- 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");
447
+ it("publishes permissions with the current head bundle as the expected parent", 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 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
452
+ const currentHead = {
453
+ schemaHash,
454
+ version: 2,
455
+ parentBundleObjectId: "11111111-1111-1111-1111-111111111111",
456
+ bundleObjectId: "22222222-2222-2222-2222-222222222222",
457
+ };
458
+ const fetchMock = vi.fn(async (input, init) => {
459
+ if (input.endsWith("/schemas")) {
460
+ return new Response(JSON.stringify({ hashes: [schemaHash] }), { status: 200 });
461
+ }
462
+ if (input.endsWith(`/schema/${schemaHash}`)) {
463
+ return new Response(JSON.stringify(storedRootSchema()), { status: 200 });
464
+ }
465
+ if (input.endsWith("/admin/permissions/head")) {
466
+ return new Response(JSON.stringify({ head: currentHead }), { status: 200 });
467
+ }
468
+ if (input.endsWith("/admin/permissions")) {
469
+ const body = JSON.parse(String(init?.body));
470
+ expect(body.schemaHash).toBe(schemaHash);
471
+ expect(body.expectedParentBundleObjectId).toBe(currentHead.bundleObjectId);
472
+ expect(Object.keys(body.permissions)).toContain("todos");
473
+ return new Response(JSON.stringify({
474
+ head: {
475
+ schemaHash,
476
+ version: 3,
477
+ parentBundleObjectId: currentHead.bundleObjectId,
478
+ bundleObjectId: "33333333-3333-3333-3333-333333333333",
479
+ },
480
+ }), { status: 201 });
481
+ }
482
+ throw new Error(`Unexpected fetch: ${input}`);
483
+ });
484
+ vi.stubGlobal("fetch", fetchMock);
485
+ const { logs } = await captureConsoleLogs(() => pushPermissions({
486
+ serverUrl: "http://localhost:1625",
487
+ adminSecret: "admin-secret",
488
+ schemaDir: root,
489
+ }));
490
+ expect(logs).toContain(`Resolved structural schema hash ${schemaHash.slice(0, 12)}.`);
491
+ expect(logs).toContain(`Publishing from parent v2 on ${schemaHash.slice(0, 12)}.`);
492
+ expect(logs).toContain(`Published permissions head v3 on ${schemaHash.slice(0, 12)}.`);
493
+ expect(logs).toContain("Permission-only changes do not create schema hashes or require migrations.");
494
+ });
495
+ it("publishes permission literals using tagged wire values", async () => {
496
+ const { root } = await createWorkspace();
497
+ await writeFile(join(root, "schema.ts"), rootSchemaWithBooleanTodo());
498
+ await writeFile(join(root, "permissions.ts"), rootBooleanLiteralPermissionsSchema());
499
+ const schemaHash = "abababababababababababababababababababababababababababababababab";
500
+ const fetchMock = vi.fn(async (input, init) => {
501
+ if (input.endsWith("/schemas")) {
502
+ return new Response(JSON.stringify({ hashes: [schemaHash] }), { status: 200 });
503
+ }
504
+ if (input.endsWith(`/schema/${schemaHash}`)) {
505
+ return new Response(JSON.stringify({
506
+ todos: {
507
+ columns: [
508
+ { name: "title", column_type: { type: "Text" }, nullable: false },
509
+ { name: "done", column_type: { type: "Boolean" }, nullable: false },
510
+ ],
511
+ },
512
+ }), { status: 200 });
513
+ }
514
+ if (input.endsWith("/admin/permissions/head")) {
515
+ return new Response(JSON.stringify({ head: null }), { status: 200 });
516
+ }
517
+ if (input.endsWith("/admin/permissions")) {
518
+ const body = JSON.parse(String(init?.body));
519
+ expect(body.permissions.todos.select.using).toEqual({
520
+ type: "Cmp",
521
+ column: "done",
522
+ op: "Eq",
523
+ value: {
524
+ type: "Literal",
525
+ value: {
526
+ type: "Boolean",
527
+ value: true,
528
+ },
529
+ },
530
+ });
531
+ return new Response(JSON.stringify({
532
+ head: {
533
+ schemaHash,
534
+ version: 1,
535
+ parentBundleObjectId: null,
536
+ bundleObjectId: "99999999-9999-9999-9999-999999999999",
537
+ },
538
+ }), { status: 201 });
539
+ }
540
+ throw new Error(`Unexpected fetch: ${input}`);
541
+ });
542
+ vi.stubGlobal("fetch", fetchMock);
543
+ await pushPermissions({
544
+ serverUrl: "http://localhost:1625",
545
+ adminSecret: "admin-secret",
546
+ schemaDir: root,
547
+ });
548
+ expect(fetchMock).toHaveBeenCalled();
321
549
  });
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");
550
+ });
551
+ function runBin(args) {
552
+ return spawnSync(process.execPath, [binPath, ...args], {
553
+ encoding: "utf8",
554
+ env: process.env,
328
555
  });
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");
556
+ }
557
+ describe("bin integration", () => {
558
+ it("routes validate through the TypeScript CLI for a root schema.ts project", async () => {
559
+ const { root } = await createWorkspace();
560
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
561
+ const result = runBin(["validate", "--schema-dir", root]);
562
+ expect(result.status).toBe(0);
563
+ expect(await fileExists(join(root, "schema", "current.sql"))).toBe(false);
564
+ expect(await fileExists(join(root, "schema", "app.ts"))).toBe(false);
340
565
  });
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");
566
+ it("loads root permissions.ts through the validate command", async () => {
567
+ const { root } = await createWorkspace();
568
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
569
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema("./schema.ts", distIndexPath));
570
+ const result = runBin(["validate", "--schema-dir", root]);
571
+ expect(result.status).toBe(0);
572
+ expect(await fileExists(join(root, "permissions.test.ts"))).toBe(false);
351
573
  });
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");
574
+ it("fails when no root schema.ts can be found", async () => {
575
+ const { root } = await createWorkspace();
576
+ const result = runBin(["validate", "--schema-dir", root]);
577
+ expect(result.status).toBe(1);
578
+ expect(result.stderr).toContain("Schema file not found");
361
579
  });
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");
580
+ it("rejects the removed build alias with a validate hint", async () => {
581
+ const { root } = await createWorkspace();
582
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
583
+ const result = runBin(["build", "--schema-dir", root]);
584
+ expect(result.status).toBe(1);
585
+ expect(result.stderr).toContain("renamed to `jazz-tools validate`");
371
586
  });
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);");
587
+ it("routes schema export through the TypeScript CLI", async () => {
588
+ const { root } = await createWorkspace();
589
+ await writeFile(join(root, "schema.ts"), rootSchemaWithoutInlinePermissions(distIndexPath));
590
+ await writeFile(join(root, "permissions.ts"), rootPermissionsSchema("./schema.ts", distIndexPath));
591
+ const result = runBin(["schema", "export", "--schema-dir", root, "--format", "json"]);
592
+ expect(result.status).toBe(0);
593
+ const exported = JSON.parse(String(result.stdout));
594
+ expect(exported.todos.columns.some((column) => column.name === "ownerId")).toBe(true);
380
595
  });
381
596
  });
382
597
  //# sourceMappingURL=cli.test.js.map