@cosmicdrift/kumiko-framework 0.269.1 → 0.270.0
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/README.md +5 -1
- package/package.json +3 -3
- package/src/__tests__/anonymous-access.integration.test.ts +1 -1
- package/src/__tests__/error-contract.integration.test.ts +1 -1
- package/src/__tests__/fixtures/migrate-db-raw/global-fetch.expected.ts +9 -0
- package/src/__tests__/fixtures/migrate-db-raw/global-fetch.input.ts +10 -0
- package/src/__tests__/fixtures/migrate-db-raw/global-select.expected.ts +6 -0
- package/src/__tests__/fixtures/migrate-db-raw/global-select.input.ts +12 -0
- package/src/__tests__/fixtures/migrate-db-raw/import-pruning.expected.ts +12 -0
- package/src/__tests__/fixtures/migrate-db-raw/import-pruning.input.ts +12 -0
- package/src/__tests__/fixtures/migrate-db-raw/manual-only.expected.ts +26 -0
- package/src/__tests__/fixtures/migrate-db-raw/manual-only.input.ts +26 -0
- package/src/__tests__/fixtures/migrate-db-raw/own-tenant-const-shorthand.expected.ts +10 -0
- package/src/__tests__/fixtures/migrate-db-raw/own-tenant-const-shorthand.input.ts +11 -0
- package/src/__tests__/fixtures/migrate-db-raw/own-tenant-event.expected.ts +9 -0
- package/src/__tests__/fixtures/migrate-db-raw/own-tenant-event.input.ts +15 -0
- package/src/__tests__/fixtures/migrate-open-to-all/manual-only.expected.ts +16 -0
- package/src/__tests__/fixtures/migrate-open-to-all/manual-only.input.ts +16 -0
- package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.expected.ts +12 -0
- package/src/__tests__/fixtures/migrate-open-to-all/test-nested-shorthand.input.ts +12 -0
- package/src/__tests__/fixtures/migrate-open-to-all/test-simple.expected.ts +7 -0
- package/src/__tests__/fixtures/migrate-open-to-all/test-simple.input.ts +7 -0
- package/src/__tests__/full-stack.integration.test.ts +2 -2
- package/src/__tests__/migrate-db-raw-codemod.test.ts +132 -0
- package/src/__tests__/migrate-open-to-all-codemod.test.ts +84 -0
- package/src/__tests__/transition-guard.integration.test.ts +7 -7
- package/src/__tests__/upgrade-cli.test.ts +54 -0
- package/src/api/__tests__/api.test.ts +2 -2
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +2 -2
- package/src/api/__tests__/request-locale.integration.test.ts +1 -1
- package/src/api/__tests__/server-error-logging.test.ts +1 -1
- package/src/changes.json +10 -2
- package/src/db/__tests__/list-filter-field-access.integration.test.ts +1 -1
- package/src/derivatives/__tests__/derivatives-context.integration.test.ts +2 -2
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-projection-list.test.ts +17 -17
- package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +18 -18
- package/src/engine/__tests__/boot-validator-query-refs.test.ts +3 -3
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
- package/src/engine/__tests__/boot-validator.test.ts +18 -18
- package/src/engine/__tests__/build-app-schema.test.ts +10 -6
- package/src/engine/__tests__/build-config-feature-schema-secrets.test.ts +48 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +34 -5
- package/src/engine/__tests__/engine.test.ts +39 -25
- package/src/engine/__tests__/entity-handlers.test.ts +19 -8
- package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +2 -2
- package/src/engine/__tests__/extends-registrar.test.ts +3 -3
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +9 -3
- package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/lifecycle-hooks.test.ts +9 -3
- package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +6 -2
- package/src/engine/__tests__/projection-detail-actions.test.ts +2 -2
- package/src/engine/__tests__/projection-detail-singleton.test.ts +3 -3
- package/src/engine/__tests__/projection-detail-tabs.test.ts +3 -3
- package/src/engine/__tests__/projection-list-filter.integration.test.ts +1 -1
- package/src/engine/__tests__/registry.test.ts +19 -13
- package/src/engine/__tests__/screen.test.ts +26 -20
- package/src/engine/boot-validator/__tests__/access-declarations.test.ts +5 -4
- package/src/engine/boot-validator/__tests__/parent-ref.test.ts +18 -6
- package/src/engine/boot-validator/access-declarations.ts +3 -5
- package/src/engine/boot-validator/nav.ts +4 -3
- package/src/engine/build-config-feature-schema.ts +9 -2
- package/src/engine/entity-handlers.ts +1 -1
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +3 -1
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +2 -2
- package/src/engine/feature-ast/__tests__/parse.test.ts +106 -4
- package/src/engine/feature-ast/__tests__/patcher.test.ts +3 -3
- package/src/engine/feature-ast/__tests__/read-optional-access-rule.test.ts +4 -4
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +5 -1
- package/src/engine/feature-ast/extractors/handlers.ts +16 -2
- package/src/engine/feature-ast/extractors/hooks.ts +25 -4
- package/src/engine/feature-ast/patcher.ts +15 -3
- package/src/engine/feature-ast/patterns.ts +4 -0
- package/src/engine/feature-ast/render.ts +3 -0
- package/src/engine/pattern-library/shared-fields.ts +7 -3
- package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +1 -1
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +3 -3
- package/src/entrypoint/__tests__/split-deploy.integration.test.ts +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +2 -2
- package/src/jobs/__tests__/job-event-trigger.integration.test.ts +2 -2
- package/src/jobs/__tests__/job-multi-trigger.integration.test.ts +2 -2
- package/src/jobs/__tests__/jobs.integration.test.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +7 -7
- package/src/pipeline/__tests__/archive-stream.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +15 -15
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +10 -10
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +1 -1
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/query-projection.integration.test.ts +3 -3
- package/src/scripts/codemod/migrate-db-raw.ts +397 -0
- package/src/scripts/codemod/migrate-open-to-all.ts +248 -0
- package/src/secrets/__tests__/contains-secret.test.ts +5 -5
- package/src/stack/__tests__/request-helper.integration.test.ts +3 -3
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +1 -1
- package/src/testing/__tests__/e2e-generator.test.ts +19 -5
package/README.md
CHANGED
|
@@ -45,7 +45,11 @@ export const taskFeature = defineFeature("tasks", (r) => {
|
|
|
45
45
|
// control, all explicit via the options below.
|
|
46
46
|
r.crud("task", taskEntity, {
|
|
47
47
|
write: { access: { roles: ["User"] } },
|
|
48
|
-
read: {
|
|
48
|
+
read: {
|
|
49
|
+
access: {
|
|
50
|
+
openToAll: { reason: "any signed-in user may list and view tasks; only writes are role-gated" },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
// Read-model fed from task events, rebuildable via the CLI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.270.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-types": "0.270.0",
|
|
202
202
|
"bullmq": "^5.76.7",
|
|
203
203
|
"bun-types": "^1.3.13",
|
|
204
204
|
"hono": "^4.13.1",
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"zod": "^4.4.3"
|
|
215
215
|
},
|
|
216
216
|
"devDependencies": {
|
|
217
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
217
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.270.0",
|
|
218
218
|
"bun-types": "^1.3.13",
|
|
219
219
|
"pino-pretty": "^13.1.3"
|
|
220
220
|
},
|
|
@@ -66,7 +66,7 @@ const shopFeature = defineFeature("anonshop", (r) => {
|
|
|
66
66
|
const rows = await selectMany(ctx.db, productTable);
|
|
67
67
|
return rows;
|
|
68
68
|
},
|
|
69
|
-
{ access: { openToAll:
|
|
69
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
70
70
|
);
|
|
71
71
|
|
|
72
72
|
// Anonymous can place a guest order.
|
|
@@ -149,7 +149,7 @@ const errorFeature = defineFeature("errctr", (r) => {
|
|
|
149
149
|
async (event) => {
|
|
150
150
|
throw new NotFoundError("item", event.payload.id);
|
|
151
151
|
},
|
|
152
|
-
{ access: { openToAll:
|
|
152
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
153
153
|
);
|
|
154
154
|
|
|
155
155
|
// InternalError auto-wrap: handler raises an unexpected TypeError; the
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { userTable } from "./entities/user";
|
|
2
|
+
|
|
3
|
+
export const userDetail = defineQueryHandler({
|
|
4
|
+
name: "user:detail",
|
|
5
|
+
handler: async (query, ctx) => {
|
|
6
|
+
const row = await ctx.db.global(userTable).fetchOne<UserRow>({ id: query.payload.id });
|
|
7
|
+
return row ?? null;
|
|
8
|
+
},
|
|
9
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { userTable } from "./entities/user";
|
|
3
|
+
|
|
4
|
+
export const userDetail = defineQueryHandler({
|
|
5
|
+
name: "user:detail",
|
|
6
|
+
handler: async (query, ctx) => {
|
|
7
|
+
const row = await fetchOne<UserRow>(ctx.db.raw, userTable, { id: query.payload.id });
|
|
8
|
+
return row ?? null;
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { globalFeatureStateTable } from "./entities/global-feature-state";
|
|
3
|
+
|
|
4
|
+
export async function loadFlags(ctx: Ctx) {
|
|
5
|
+
const rows = await selectMany<FlagRow>(
|
|
6
|
+
ctx.db.raw,
|
|
7
|
+
globalFeatureStateTable,
|
|
8
|
+
{ key: "beta" },
|
|
9
|
+
{ limit: 10 },
|
|
10
|
+
);
|
|
11
|
+
return rows;
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { userTable } from "./entities/user";
|
|
3
|
+
|
|
4
|
+
export const userDetail = defineQueryHandler({
|
|
5
|
+
name: "user:detail",
|
|
6
|
+
handler: async (query, ctx) => {
|
|
7
|
+
const row = await ctx.db.global(userTable).fetchOne<UserRow>({ id: query.payload.id });
|
|
8
|
+
return row ?? null;
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const selectManyHelper = selectMany;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { userTable } from "./entities/user";
|
|
3
|
+
|
|
4
|
+
export const userDetail = defineQueryHandler({
|
|
5
|
+
name: "user:detail",
|
|
6
|
+
handler: async (query, ctx) => {
|
|
7
|
+
const row = await fetchOne<UserRow>(ctx.db.raw, userTable, { id: query.payload.id });
|
|
8
|
+
return row ?? null;
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const selectManyHelper = selectMany;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { countWhere, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { auditTable, invoiceTable } from "./entities";
|
|
4
|
+
|
|
5
|
+
export const auditWrite = defineWriteHandler({
|
|
6
|
+
name: "audit:write",
|
|
7
|
+
handler: async (event, ctx) => {
|
|
8
|
+
const tenantId = event.payload.tenantId;
|
|
9
|
+
await insertOne(ctx.db.raw, auditTable, { tenantId, action: event.payload.action });
|
|
10
|
+
const total = await countWhere(ctx.db.raw, invoiceTable, { status: "open" });
|
|
11
|
+
return { total };
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function bootstrapSystemDb(ctx: Ctx, x: unknown) {
|
|
16
|
+
return createTenantDb(ctx.db.raw, x, "system");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const crossTenantAck = defineWriteHandler({
|
|
20
|
+
name: "cross-tenant:ack",
|
|
21
|
+
handler: async (event, ctx) => {
|
|
22
|
+
const r = { id: event.payload.id };
|
|
23
|
+
const acked = ctx.systemDb.acknowledgeCrossTenant(r).raw;
|
|
24
|
+
return { acked };
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { countWhere, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { auditTable, invoiceTable } from "./entities";
|
|
4
|
+
|
|
5
|
+
export const auditWrite = defineWriteHandler({
|
|
6
|
+
name: "audit:write",
|
|
7
|
+
handler: async (event, ctx) => {
|
|
8
|
+
const tenantId = event.payload.tenantId;
|
|
9
|
+
await insertOne(ctx.db.raw, auditTable, { tenantId, action: event.payload.action });
|
|
10
|
+
const total = await countWhere(ctx.db.raw, invoiceTable, { status: "open" });
|
|
11
|
+
return { total };
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function bootstrapSystemDb(ctx: Ctx, x: unknown) {
|
|
16
|
+
return createTenantDb(ctx.db.raw, x, "system");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const crossTenantAck = defineWriteHandler({
|
|
20
|
+
name: "cross-tenant:ack",
|
|
21
|
+
handler: async (event, ctx) => {
|
|
22
|
+
const r = { id: event.payload.id };
|
|
23
|
+
const acked = ctx.systemDb.acknowledgeCrossTenant(r).raw;
|
|
24
|
+
return { acked };
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { invoiceTable } from "./entities/invoice";
|
|
2
|
+
|
|
3
|
+
export const invoiceDetailQuery = defineQueryHandler({
|
|
4
|
+
name: "invoice:detail",
|
|
5
|
+
handler: async (query, ctx) => {
|
|
6
|
+
const tenantId = query.user.tenantId;
|
|
7
|
+
const row = await ctx.db.fetchOne<InvoiceRow>(invoiceTable, { id: query.payload.id, tenantId });
|
|
8
|
+
return row ?? null;
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { invoiceTable } from "./entities/invoice";
|
|
3
|
+
|
|
4
|
+
export const invoiceDetailQuery = defineQueryHandler({
|
|
5
|
+
name: "invoice:detail",
|
|
6
|
+
handler: async (query, ctx) => {
|
|
7
|
+
const tenantId = query.user.tenantId;
|
|
8
|
+
const row = await fetchOne<InvoiceRow>(ctx.db.raw, invoiceTable, { id: query.payload.id, tenantId });
|
|
9
|
+
return row ?? null;
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { invoiceTable } from "./entities/invoice";
|
|
2
|
+
|
|
3
|
+
export const invoiceListWrite = defineWriteHandler({
|
|
4
|
+
name: "invoice:list",
|
|
5
|
+
handler: async (event, ctx) => {
|
|
6
|
+
const rows = await ctx.db.selectMany(invoiceTable, { tenantId: event.user.tenantId, status: "open" }, { limit: 20 });
|
|
7
|
+
return rows;
|
|
8
|
+
},
|
|
9
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { invoiceTable } from "./entities/invoice";
|
|
3
|
+
|
|
4
|
+
export const invoiceListWrite = defineWriteHandler({
|
|
5
|
+
name: "invoice:list",
|
|
6
|
+
handler: async (event, ctx) => {
|
|
7
|
+
const rows = await selectMany(
|
|
8
|
+
ctx.db.raw,
|
|
9
|
+
invoiceTable,
|
|
10
|
+
{ tenantId: event.user.tenantId, status: "open" },
|
|
11
|
+
{ limit: 20 },
|
|
12
|
+
);
|
|
13
|
+
return rows;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const listWidgets = {
|
|
4
|
+
name: "widget:list",
|
|
5
|
+
schema: z.object({}),
|
|
6
|
+
description: "Lists all widgets; visible to everyone",
|
|
7
|
+
access: { openToAll: true },
|
|
8
|
+
handler: async () => [],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const legacyPing = {
|
|
12
|
+
name: "legacy:ping",
|
|
13
|
+
schema: z.object({}),
|
|
14
|
+
access: { openToAll: true },
|
|
15
|
+
handler: async () => "pong",
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const listWidgets = {
|
|
4
|
+
name: "widget:list",
|
|
5
|
+
schema: z.object({}),
|
|
6
|
+
description: "Lists all widgets; visible to everyone",
|
|
7
|
+
access: { openToAll: true },
|
|
8
|
+
handler: async () => [],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const legacyPing = {
|
|
12
|
+
name: "legacy:ping",
|
|
13
|
+
schema: z.object({}),
|
|
14
|
+
access: { openToAll: true },
|
|
15
|
+
handler: async () => "pong",
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
verbAccess: {
|
|
3
|
+
create: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
4
|
+
list: { roles: ["admin"] },
|
|
5
|
+
},
|
|
6
|
+
read: {
|
|
7
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
8
|
+
},
|
|
9
|
+
write: {
|
|
10
|
+
access: { roles: ["admin"] },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -174,14 +174,14 @@ const userFeature = defineFeature("users", (r) => {
|
|
|
174
174
|
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
175
175
|
}),
|
|
176
176
|
async (query, ctx) => userExecutor(ctx).list(query.payload, query.user, ctx.db),
|
|
177
|
-
{ access: { openToAll:
|
|
177
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
178
178
|
);
|
|
179
179
|
|
|
180
180
|
r.queryHandler(
|
|
181
181
|
"user:detail",
|
|
182
182
|
z.object({ id: z.uuid() }),
|
|
183
183
|
async (query, ctx) => userExecutor(ctx).detail(query.payload, query.user, ctx.db),
|
|
184
|
-
{ access: { openToAll:
|
|
184
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
185
185
|
);
|
|
186
186
|
|
|
187
187
|
r.hook("postSave", { allOf: user }, async (result) => {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { migrateDbRawSource } from "../scripts/codemod/migrate-db-raw";
|
|
5
|
+
|
|
6
|
+
const FIXTURES_DIR = join(import.meta.dir, "fixtures", "migrate-db-raw");
|
|
7
|
+
const DEFAULT_GLOBAL_TABLES = new Set(["userTable", "globalFeatureStateTable"]);
|
|
8
|
+
|
|
9
|
+
function readFixture(name: string): { input: string; expected: string } {
|
|
10
|
+
return {
|
|
11
|
+
input: readFileSync(join(FIXTURES_DIR, `${name}.input.ts`), "utf8"),
|
|
12
|
+
expected: readFileSync(join(FIXTURES_DIR, `${name}.expected.ts`), "utf8"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("migrateDbRawSource", () => {
|
|
17
|
+
it("rewrites a global fetchOne with type args to db.global().fetchOne()", () => {
|
|
18
|
+
const { input, expected } = readFixture("global-fetch");
|
|
19
|
+
const result = migrateDbRawSource(input, "global-fetch.ts", {
|
|
20
|
+
globalTables: DEFAULT_GLOBAL_TABLES,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result.output).toBe(expected);
|
|
24
|
+
expect(result.rewrites).toHaveLength(1);
|
|
25
|
+
expect(result.rewrites[0]?.rule).toBe("global-table");
|
|
26
|
+
expect(result.rewrites[0]?.line).toBe(7);
|
|
27
|
+
expect(result.manual).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rewrites a global selectMany with options to db.global().selectMany()", () => {
|
|
31
|
+
const { input, expected } = readFixture("global-select");
|
|
32
|
+
const result = migrateDbRawSource(input, "global-select.ts", {
|
|
33
|
+
globalTables: DEFAULT_GLOBAL_TABLES,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(result.output).toBe(expected);
|
|
37
|
+
expect(result.rewrites).toHaveLength(1);
|
|
38
|
+
expect(result.rewrites[0]?.rule).toBe("global-table");
|
|
39
|
+
expect(result.manual).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rewrites own-tenant filtering via event.user.tenantId", () => {
|
|
43
|
+
const { input, expected } = readFixture("own-tenant-event");
|
|
44
|
+
const result = migrateDbRawSource(input, "own-tenant-event.ts", {
|
|
45
|
+
globalTables: DEFAULT_GLOBAL_TABLES,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result.output).toBe(expected);
|
|
49
|
+
expect(result.rewrites).toHaveLength(1);
|
|
50
|
+
expect(result.rewrites[0]?.rule).toBe("own-tenant");
|
|
51
|
+
expect(result.manual).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("rewrites own-tenant filtering via a const tenantId shorthand binding", () => {
|
|
55
|
+
const { input, expected } = readFixture("own-tenant-const-shorthand");
|
|
56
|
+
const result = migrateDbRawSource(input, "own-tenant-const-shorthand.ts", {
|
|
57
|
+
globalTables: DEFAULT_GLOBAL_TABLES,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result.output).toBe(expected);
|
|
61
|
+
expect(result.rewrites).toHaveLength(1);
|
|
62
|
+
expect(result.rewrites[0]?.rule).toBe("own-tenant");
|
|
63
|
+
expect(result.manual).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("leaves unsafe call sites untouched and reports them as manual", () => {
|
|
67
|
+
const { input, expected } = readFixture("manual-only");
|
|
68
|
+
const result = migrateDbRawSource(input, "manual-only.ts", {
|
|
69
|
+
globalTables: DEFAULT_GLOBAL_TABLES,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.output).toBe(expected);
|
|
73
|
+
expect(result.rewrites).toHaveLength(0);
|
|
74
|
+
expect(result.manual).toHaveLength(4);
|
|
75
|
+
|
|
76
|
+
const byText = new Map(result.manual.map((m) => [m.text, m]));
|
|
77
|
+
|
|
78
|
+
const insertOneSite = byText.get(
|
|
79
|
+
"insertOne(ctx.db.raw, auditTable, { tenantId, action: event.payload.action })",
|
|
80
|
+
);
|
|
81
|
+
expect(insertOneSite).toBeDefined();
|
|
82
|
+
expect(insertOneSite?.line).toBe(9);
|
|
83
|
+
expect(insertOneSite?.enclosing).toBe("handler");
|
|
84
|
+
|
|
85
|
+
const countWhereSite = byText.get('countWhere(ctx.db.raw, invoiceTable, { status: "open" })');
|
|
86
|
+
expect(countWhereSite).toBeDefined();
|
|
87
|
+
expect(countWhereSite?.line).toBe(10);
|
|
88
|
+
|
|
89
|
+
const createTenantDbSite = byText.get('createTenantDb(ctx.db.raw, x, "system")');
|
|
90
|
+
expect(createTenantDbSite).toBeDefined();
|
|
91
|
+
expect(createTenantDbSite?.line).toBe(16);
|
|
92
|
+
expect(createTenantDbSite?.enclosing).toBe("bootstrapSystemDb");
|
|
93
|
+
|
|
94
|
+
const systemDbSite = byText.get("ctx.systemDb.acknowledgeCrossTenant(r).raw");
|
|
95
|
+
expect(systemDbSite).toBeDefined();
|
|
96
|
+
expect(systemDbSite?.line).toBe(23);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does not rewrite when tenantId comes from a non-user-scope source (payload/token)", () => {
|
|
100
|
+
const { input } = readFixture("manual-only");
|
|
101
|
+
const result = migrateDbRawSource(input, "manual-only.ts", {
|
|
102
|
+
globalTables: DEFAULT_GLOBAL_TABLES,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.output).toContain("await insertOne(ctx.db.raw, auditTable");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("prunes an unused free-function import after rewriting its only call site", () => {
|
|
109
|
+
const { input, expected } = readFixture("global-fetch");
|
|
110
|
+
const result = migrateDbRawSource(input, "global-fetch.ts", {
|
|
111
|
+
globalTables: DEFAULT_GLOBAL_TABLES,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.output).not.toContain(
|
|
115
|
+
'import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";',
|
|
116
|
+
);
|
|
117
|
+
expect(result.output).toBe(expected);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("keeps a still-referenced sibling import while pruning the rewritten one", () => {
|
|
121
|
+
const { input, expected } = readFixture("import-pruning");
|
|
122
|
+
const result = migrateDbRawSource(input, "import-pruning.ts", {
|
|
123
|
+
globalTables: DEFAULT_GLOBAL_TABLES,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(result.output).toBe(expected);
|
|
127
|
+
expect(result.output).toContain(
|
|
128
|
+
'import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";',
|
|
129
|
+
);
|
|
130
|
+
expect(result.output.split("\n")[0]).not.toContain("fetchOne");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { migrateOpenToAllSource } from "../scripts/codemod/migrate-open-to-all";
|
|
5
|
+
|
|
6
|
+
const FIXTURES_DIR = join(import.meta.dir, "fixtures", "migrate-open-to-all");
|
|
7
|
+
const DEFAULT_REASON = "test handler callable by any signed-in test user";
|
|
8
|
+
|
|
9
|
+
function readFixture(name: string): { input: string; expected: string } {
|
|
10
|
+
return {
|
|
11
|
+
input: readFileSync(join(FIXTURES_DIR, `${name}.input.ts`), "utf8"),
|
|
12
|
+
expected: readFileSync(join(FIXTURES_DIR, `${name}.expected.ts`), "utf8"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("migrateOpenToAllSource", () => {
|
|
17
|
+
it("rewrites a top-level openToAll: true in a *.test.ts file to the default test reason", () => {
|
|
18
|
+
const { input, expected } = readFixture("test-simple");
|
|
19
|
+
const result = migrateOpenToAllSource(input, "widget-list.test.ts", {
|
|
20
|
+
testReason: DEFAULT_REASON,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result.output).toBe(expected);
|
|
24
|
+
expect(result.rewrites).toHaveLength(1);
|
|
25
|
+
expect(result.rewrites[0]?.after).toBe(`openToAll: { reason: "${DEFAULT_REASON}" }`);
|
|
26
|
+
expect(result.manual).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("rewrites openToAll: true under a __tests__/ path even without a .test.ts suffix", () => {
|
|
30
|
+
const { input, expected } = readFixture("test-simple");
|
|
31
|
+
const result = migrateOpenToAllSource(input, "packages/framework/__tests__/widget-list.ts", {
|
|
32
|
+
testReason: DEFAULT_REASON,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result.output).toBe(expected);
|
|
36
|
+
expect(result.rewrites).toHaveLength(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("honors a custom --test-reason for test files", () => {
|
|
40
|
+
const { input } = readFixture("test-simple");
|
|
41
|
+
const result = migrateOpenToAllSource(input, "widget-list.test.ts", {
|
|
42
|
+
testReason: "custom reason for this suite",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.output).toContain(
|
|
46
|
+
'access: { openToAll: { reason: "custom reason for this suite" } }',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rewrites nested shorthand openToAll: true (verbAccess, read.access) in test files", () => {
|
|
51
|
+
const { input, expected } = readFixture("test-nested-shorthand");
|
|
52
|
+
const result = migrateOpenToAllSource(input, "config.test.ts", { testReason: DEFAULT_REASON });
|
|
53
|
+
|
|
54
|
+
expect(result.output).toBe(expected);
|
|
55
|
+
expect(result.rewrites).toHaveLength(2);
|
|
56
|
+
expect(result.manual).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves non-test files untouched and reports manual sites with derived handler name + description", () => {
|
|
60
|
+
const { input, expected } = readFixture("manual-only");
|
|
61
|
+
const result = migrateOpenToAllSource(input, "handlers.ts", { testReason: DEFAULT_REASON });
|
|
62
|
+
|
|
63
|
+
expect(result.output).toBe(expected);
|
|
64
|
+
expect(result.rewrites).toHaveLength(0);
|
|
65
|
+
expect(result.manual).toHaveLength(2);
|
|
66
|
+
|
|
67
|
+
const withDescription = result.manual.find((m) => m.handlerName === "widget:list");
|
|
68
|
+
expect(withDescription).toBeDefined();
|
|
69
|
+
expect(withDescription?.description).toBe("Lists all widgets; visible to everyone");
|
|
70
|
+
expect(withDescription?.line).toBe(7);
|
|
71
|
+
|
|
72
|
+
const withoutDescription = result.manual.find((m) => m.handlerName === "legacy:ping");
|
|
73
|
+
expect(withoutDescription).toBeDefined();
|
|
74
|
+
expect(withoutDescription?.description).toBeUndefined();
|
|
75
|
+
expect(withoutDescription?.line).toBe(14);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("never rewrites in a non-test file, even when a description is present", () => {
|
|
79
|
+
const { input } = readFixture("manual-only");
|
|
80
|
+
const result = migrateOpenToAllSource(input, "handlers.ts", { testReason: DEFAULT_REASON });
|
|
81
|
+
|
|
82
|
+
expect(result.output).toContain("access: { openToAll: true }");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -86,7 +86,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
86
86
|
event.user,
|
|
87
87
|
ctx.db,
|
|
88
88
|
),
|
|
89
|
-
{ access: { openToAll:
|
|
89
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
90
90
|
);
|
|
91
91
|
|
|
92
92
|
r.writeHandler(
|
|
@@ -102,7 +102,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
102
102
|
event.user,
|
|
103
103
|
ctx.db,
|
|
104
104
|
),
|
|
105
|
-
{ access: { openToAll:
|
|
105
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
106
106
|
);
|
|
107
107
|
|
|
108
108
|
r.writeHandler(
|
|
@@ -114,7 +114,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
114
114
|
event.user,
|
|
115
115
|
ctx.db,
|
|
116
116
|
),
|
|
117
|
-
{ access: { openToAll:
|
|
117
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
118
118
|
);
|
|
119
119
|
|
|
120
120
|
r.writeHandler(
|
|
@@ -130,7 +130,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
130
130
|
event.user,
|
|
131
131
|
ctx.db,
|
|
132
132
|
),
|
|
133
|
-
{ access: { openToAll:
|
|
133
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
134
134
|
);
|
|
135
135
|
|
|
136
136
|
r.writeHandler(
|
|
@@ -142,7 +142,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
142
142
|
event.user,
|
|
143
143
|
ctx.db,
|
|
144
144
|
),
|
|
145
|
-
{ access: { openToAll:
|
|
145
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
146
146
|
);
|
|
147
147
|
|
|
148
148
|
r.writeHandler(
|
|
@@ -154,7 +154,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
154
154
|
event.user,
|
|
155
155
|
ctx.db,
|
|
156
156
|
),
|
|
157
|
-
{ access: { openToAll:
|
|
157
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
158
158
|
);
|
|
159
159
|
|
|
160
160
|
r.writeHandler(
|
|
@@ -170,7 +170,7 @@ const feature = defineFeature("txguard", (r) => {
|
|
|
170
170
|
event.user,
|
|
171
171
|
ctx.db,
|
|
172
172
|
),
|
|
173
|
-
{ access: { openToAll:
|
|
173
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
174
174
|
);
|
|
175
175
|
});
|
|
176
176
|
|