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.
- package/bin/docs-index.db +0 -0
- package/bin/docs-index.txt +1624 -542
- package/bin/jazz-tools.js +19 -40
- package/bin/native/jazz-tools-darwin-arm64 +0 -0
- package/bin/native/jazz-tools-darwin-x64 +0 -0
- package/bin/native/jazz-tools-linux-arm64 +0 -0
- package/bin/native/jazz-tools-linux-x64 +0 -0
- package/bin/native/jazz-tools-windows-x64.exe +0 -0
- package/dist/backend/create-jazz-context.d.ts +31 -6
- package/dist/backend/create-jazz-context.d.ts.map +1 -1
- package/dist/backend/create-jazz-context.js +35 -5
- package/dist/backend/create-jazz-context.js.map +1 -1
- package/dist/backend/create-jazz-context.test.js +61 -6
- package/dist/backend/create-jazz-context.test.js.map +1 -1
- package/dist/cli.d.ts +29 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +648 -246
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +512 -297
- package/dist/cli.test.js.map +1 -1
- package/dist/codegen/schema-reader.d.ts.map +1 -1
- package/dist/codegen/schema-reader.js +6 -1
- package/dist/codegen/schema-reader.js.map +1 -1
- package/dist/dev-tools/dev-tools.d.ts.map +1 -1
- package/dist/dev-tools/dev-tools.js +61 -13
- package/dist/dev-tools/dev-tools.js.map +1 -1
- package/dist/dev-tools/dev-tools.test.js +166 -0
- package/dist/dev-tools/dev-tools.test.js.map +1 -1
- package/dist/dev-tools/extension-panel.d.ts.map +1 -1
- package/dist/dev-tools/extension-panel.js +30 -7
- package/dist/dev-tools/extension-panel.js.map +1 -1
- package/dist/dev-tools/protocol.d.ts +49 -1
- package/dist/dev-tools/protocol.d.ts.map +1 -1
- package/dist/dev-tools/protocol.js +3 -0
- package/dist/dev-tools/protocol.js.map +1 -1
- package/dist/drivers/index.d.ts +1 -1
- package/dist/drivers/index.d.ts.map +1 -1
- package/dist/drivers/schema-wire.d.ts.map +1 -1
- package/dist/drivers/schema-wire.js +12 -1
- package/dist/drivers/schema-wire.js.map +1 -1
- package/dist/drivers/schema-wire.test.d.ts +2 -0
- package/dist/drivers/schema-wire.test.d.ts.map +1 -0
- package/dist/drivers/schema-wire.test.js +31 -0
- package/dist/drivers/schema-wire.test.js.map +1 -0
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/dsl.d.ts +139 -95
- package/dist/dsl.d.ts.map +1 -1
- package/dist/dsl.js +64 -8
- package/dist/dsl.js.map +1 -1
- package/dist/dsl.test.js +78 -8
- package/dist/dsl.test.js.map +1 -1
- package/dist/index.d.ts +32 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/magic-columns.d.ts +3 -1
- package/dist/magic-columns.d.ts.map +1 -1
- package/dist/magic-columns.js +20 -4
- package/dist/magic-columns.js.map +1 -1
- package/dist/mcp/build-index.test.js +1 -1
- package/dist/migrations.d.ts +126 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +112 -0
- package/dist/migrations.js.map +1 -0
- package/dist/permissions/index.test.js +35 -0
- package/dist/permissions/index.test.js.map +1 -1
- package/dist/react-native/create-jazz-client.test.js +62 -42
- package/dist/react-native/create-jazz-client.test.js.map +1 -1
- package/dist/react-native/jazz-rn-runtime-adapter.d.ts +18 -3
- package/dist/react-native/jazz-rn-runtime-adapter.d.ts.map +1 -1
- package/dist/react-native/jazz-rn-runtime-adapter.js +110 -6
- package/dist/react-native/jazz-rn-runtime-adapter.js.map +1 -1
- package/dist/react-native/jazz-rn-runtime-adapter.test.js +149 -4
- package/dist/react-native/jazz-rn-runtime-adapter.test.js.map +1 -1
- package/dist/reconcile-array.d.ts +29 -0
- package/dist/reconcile-array.d.ts.map +1 -0
- package/dist/reconcile-array.js +110 -0
- package/dist/reconcile-array.js.map +1 -0
- package/dist/reconcile-array.test.d.ts +2 -0
- package/dist/reconcile-array.test.d.ts.map +1 -0
- package/dist/reconcile-array.test.js +118 -0
- package/dist/reconcile-array.test.js.map +1 -0
- package/dist/runtime/client.d.ts +24 -20
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.for-request.test.js +8 -8
- package/dist/runtime/client.for-request.test.js.map +1 -1
- package/dist/runtime/client.js +58 -25
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/client.mutations.test.js +72 -1
- package/dist/runtime/client.mutations.test.js.map +1 -1
- package/dist/runtime/cloud-server.integration.test.js +145 -88
- package/dist/runtime/cloud-server.integration.test.js.map +1 -1
- package/dist/runtime/db.d.ts +3 -7
- package/dist/runtime/db.d.ts.map +1 -1
- package/dist/runtime/db.js +16 -14
- package/dist/runtime/db.js.map +1 -1
- package/dist/runtime/db.schema-order.test.js +8 -8
- package/dist/runtime/db.schema-order.test.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/napi.integration.test.js +113 -136
- package/dist/runtime/napi.integration.test.js.map +1 -1
- package/dist/runtime/query-adapter.d.ts.map +1 -1
- package/dist/runtime/query-adapter.js +22 -2
- package/dist/runtime/query-adapter.js.map +1 -1
- package/dist/runtime/query-adapter.test.js +81 -5
- package/dist/runtime/query-adapter.test.js.map +1 -1
- package/dist/runtime/row-transformer.js +2 -2
- package/dist/runtime/row-transformer.js.map +1 -1
- package/dist/runtime/row-transformer.test.js +9 -9
- package/dist/runtime/row-transformer.test.js.map +1 -1
- package/dist/runtime/schema-fetch.d.ts +103 -1
- package/dist/runtime/schema-fetch.d.ts.map +1 -1
- package/dist/runtime/schema-fetch.js +106 -0
- package/dist/runtime/schema-fetch.js.map +1 -1
- package/dist/runtime/sync-transport.d.ts.map +1 -1
- package/dist/runtime/sync-transport.js +15 -0
- package/dist/runtime/sync-transport.js.map +1 -1
- package/dist/runtime/sync-transport.test.js +33 -0
- package/dist/runtime/sync-transport.test.js.map +1 -1
- package/dist/runtime/value-converter.d.ts +9 -6
- package/dist/runtime/value-converter.d.ts.map +1 -1
- package/dist/runtime/value-converter.js +22 -9
- package/dist/runtime/value-converter.js.map +1 -1
- package/dist/runtime/value-converter.test.js +32 -26
- package/dist/runtime/value-converter.test.js.map +1 -1
- package/dist/schema-loader.d.ts +14 -0
- package/dist/schema-loader.d.ts.map +1 -0
- package/dist/schema-loader.js +217 -0
- package/dist/schema-loader.js.map +1 -0
- package/dist/schema-permissions.d.ts +8 -0
- package/dist/schema-permissions.d.ts.map +1 -0
- package/dist/schema-permissions.js +266 -0
- package/dist/schema-permissions.js.map +1 -0
- package/dist/schema-permissions.test.d.ts +2 -0
- package/dist/schema-permissions.test.d.ts.map +1 -0
- package/dist/schema-permissions.test.js +43 -0
- package/dist/schema-permissions.test.js.map +1 -0
- package/dist/schema.d.ts +11 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/svelte/context.svelte.test.js +50 -0
- package/dist/svelte/rune-patterns.svelte.test.js +301 -0
- package/dist/svelte/test-helpers.svelte.js +14 -0
- package/dist/svelte/use-all.svelte.d.ts.map +1 -1
- package/dist/svelte/use-all.svelte.js +7 -1
- package/dist/testing/fixtures/basic/schema.d.ts +11 -0
- package/dist/testing/fixtures/basic/schema.d.ts.map +1 -0
- package/dist/testing/fixtures/basic/schema.js +10 -0
- package/dist/testing/fixtures/basic/schema.js.map +1 -0
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/index.test.js +109 -9
- package/dist/testing/index.test.js.map +1 -1
- package/dist/testing/local-jazz-server.d.ts +2 -0
- package/dist/testing/local-jazz-server.d.ts.map +1 -1
- package/dist/testing/local-jazz-server.js +21 -51
- package/dist/testing/local-jazz-server.js.map +1 -1
- package/dist/testing/policy-test-app.d.ts.map +1 -1
- package/dist/testing/policy-test-app.js +71 -3
- package/dist/testing/policy-test-app.js.map +1 -1
- package/dist/typed-app.d.ts +364 -0
- package/dist/typed-app.d.ts.map +1 -0
- package/dist/{testing/fixtures/basic/app.js → typed-app.js} +118 -30
- package/dist/typed-app.js.map +1 -0
- package/dist/vue/use-all.d.ts +2 -2
- package/dist/vue/use-all.d.ts.map +1 -1
- package/dist/vue/use-all.js +9 -3
- package/dist/vue/use-all.js.map +1 -1
- package/dist/vue/use-all.test.js +137 -0
- package/dist/vue/use-all.test.js.map +1 -1
- package/package.json +15 -13
- package/dist/codegen/codegen.test.d.ts +0 -2
- package/dist/codegen/codegen.test.d.ts.map +0 -1
- package/dist/codegen/codegen.test.js +0 -1134
- package/dist/codegen/codegen.test.js.map +0 -1
- package/dist/codegen/index.d.ts +0 -18
- package/dist/codegen/index.d.ts.map +0 -1
- package/dist/codegen/index.js +0 -22
- package/dist/codegen/index.js.map +0 -1
- package/dist/codegen/query-builder-generator.d.ts +0 -26
- package/dist/codegen/query-builder-generator.d.ts.map +0 -1
- package/dist/codegen/query-builder-generator.js +0 -377
- package/dist/codegen/query-builder-generator.js.map +0 -1
- package/dist/codegen/type-generator.d.ts +0 -30
- package/dist/codegen/type-generator.d.ts.map +0 -1
- package/dist/codegen/type-generator.js +0 -368
- package/dist/codegen/type-generator.js.map +0 -1
- package/dist/runtime/napi.fjall.db.all.integration.test.d.ts +0 -2
- package/dist/runtime/napi.fjall.db.all.integration.test.d.ts.map +0 -1
- package/dist/runtime/napi.fjall.db.all.integration.test.js +0 -76
- package/dist/runtime/napi.fjall.db.all.integration.test.js.map +0 -1
- package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts +0 -2
- package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts.map +0 -1
- package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js +0 -47
- package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js.map +0 -1
- package/dist/runtime/napi.fjall.test-helpers.d.ts +0 -34
- package/dist/runtime/napi.fjall.test-helpers.d.ts.map +0 -1
- package/dist/runtime/napi.fjall.test-helpers.js +0 -172
- package/dist/runtime/napi.fjall.test-helpers.js.map +0 -1
- package/dist/sql-gen.d.ts +0 -5
- package/dist/sql-gen.d.ts.map +0 -1
- package/dist/sql-gen.js +0 -234
- package/dist/sql-gen.js.map +0 -1
- package/dist/sql-gen.test.d.ts +0 -2
- package/dist/sql-gen.test.d.ts.map +0 -1
- package/dist/sql-gen.test.js +0 -481
- package/dist/sql-gen.test.js.map +0 -1
- package/dist/svelte/context.test.d.ts +0 -2
- package/dist/svelte/context.test.d.ts.map +0 -1
- package/dist/svelte/context.test.js +0 -55
- package/dist/svelte/use-all.test.d.ts +0 -2
- package/dist/svelte/use-all.test.d.ts.map +0 -1
- package/dist/svelte/use-all.test.js +0 -147
- package/dist/testing/fixtures/basic/app.d.ts +0 -59
- package/dist/testing/fixtures/basic/app.d.ts.map +0 -1
- package/dist/testing/fixtures/basic/app.js.map +0 -1
- package/dist/testing/fixtures/basic/current.d.ts +0 -2
- package/dist/testing/fixtures/basic/current.d.ts.map +0 -1
- package/dist/testing/fixtures/basic/current.js +0 -6
- 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 {
|
|
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 {
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
49
|
+
function rootSchemaWithoutInlinePermissions(indexImportPath = indexPath) {
|
|
37
50
|
return `
|
|
38
|
-
import {
|
|
51
|
+
import { schema as s } from ${JSON.stringify(indexImportPath)};
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
67
|
+
function rootSchemaWithBooleanTodo(indexImportPath = indexPath) {
|
|
51
68
|
return `
|
|
52
|
-
import {
|
|
69
|
+
import { schema as s } from ${JSON.stringify(indexImportPath)};
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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 {
|
|
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
|
|
74
|
-
policy.todos.allowRead.where({
|
|
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
|
|
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 {
|
|
97
|
-
import { app } from
|
|
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({
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await
|
|
223
|
-
await
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
await
|
|
237
|
-
await
|
|
238
|
-
await
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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("
|
|
249
|
-
const {
|
|
250
|
-
await writeFile(join(
|
|
251
|
-
await writeFile(join(
|
|
252
|
-
await
|
|
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("
|
|
257
|
-
const {
|
|
258
|
-
await writeFile(join(
|
|
259
|
-
await
|
|
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
|
|
265
|
-
const {
|
|
266
|
-
await writeFile(join(
|
|
267
|
-
await
|
|
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("
|
|
270
|
-
const {
|
|
271
|
-
await writeFile(join(
|
|
272
|
-
await writeFile(join(
|
|
273
|
-
await
|
|
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
|
|
279
|
-
const {
|
|
280
|
-
await writeFile(join(
|
|
281
|
-
await writeFile(join(
|
|
282
|
-
await expect(
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
await
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
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("
|
|
299
|
-
const {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
await
|
|
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("
|
|
353
|
-
const {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
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("
|
|
363
|
-
const {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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("
|
|
373
|
-
const {
|
|
374
|
-
|
|
375
|
-
await writeFile(join(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
expect(
|
|
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
|