@cosmicdrift/kumiko-framework 0.269.2 → 0.273.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-cross-tenant/manual-only.expected.ts +44 -0
- package/src/__tests__/fixtures/migrate-cross-tenant/manual-only.input.ts +44 -0
- package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.expected.ts +41 -0
- package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.input.ts +41 -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-cross-tenant-codemod.test.ts +106 -0
- package/src/__tests__/migrate-open-to-all-codemod.test.ts +84 -0
- package/src/__tests__/related-list-parent-filter.integration.test.ts +103 -0
- package/src/__tests__/transition-guard.integration.test.ts +7 -7
- package/src/api/__tests__/api.test.ts +2 -2
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/nested-write-ownership.integration.test.ts +268 -0
- 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/api/server.ts +7 -20
- package/src/bun-db/__tests__/query-guards.test.ts +60 -3
- package/src/bun-db/query.ts +24 -21
- package/src/changes.json +99 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +77 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +1 -1
- package/src/db/__tests__/list-filter-field-access.integration.test.ts +1 -1
- package/src/db/__tests__/replay-migration-sql.test.ts +17 -0
- package/src/db/__tests__/tenant-db-convention-cross-tenant.test.ts +72 -0
- package/src/db/__tests__/tenant-db-declared-unsafe-raw.test.ts +104 -0
- package/src/db/__tests__/tenant-db-global.test.ts +92 -0
- package/src/db/__tests__/tenant-db-no-raw.test.ts +75 -0
- package/src/db/__tests__/tenant-db.integration.test.ts +3 -1
- package/src/db/__tests__/unchecked-system-db.test.ts +76 -0
- package/src/db/apply-entity-event.ts +1 -1
- package/src/db/assert-exists-in.ts +1 -1
- package/src/db/event-store-executor-context.ts +5 -2
- package/src/db/event-store-executor-read.ts +6 -4
- package/src/db/event-store-executor-write.ts +24 -18
- package/src/db/queries/shadow-swap.ts +36 -0
- package/src/db/replay-migration-sql.ts +5 -5
- package/src/db/tenant-db-runner.ts +23 -0
- package/src/db/tenant-db.ts +97 -24
- package/src/derivatives/__tests__/derivatives-context.integration.test.ts +2 -2
- package/src/derivatives/__tests__/derivatives-context.test.ts +0 -1
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-nav-area.test.ts +116 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-projection-list.test.ts +108 -17
- package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +161 -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 +69 -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-create-caller-id.integration.test.ts +87 -0
- package/src/engine/__tests__/entity-handlers-escape-hatch.test.ts +91 -0
- package/src/engine/__tests__/entity-handlers.test.ts +47 -8
- package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +2 -2
- package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +30 -5
- package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +206 -47
- 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__/read-steps-tenant-filter.integration.test.ts +215 -0
- package/src/engine/__tests__/registry.test.ts +19 -13
- package/src/engine/__tests__/required-surface-keys.test.ts +16 -0
- package/src/engine/__tests__/screen.test.ts +176 -20
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +0 -1
- package/src/engine/__tests__/steps-read.test.ts +204 -8
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +7 -4
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +6 -3
- package/src/engine/boot-validator/__tests__/access-declarations.test.ts +5 -4
- package/src/engine/boot-validator/__tests__/global-tenancy.test.ts +16 -1
- package/src/engine/boot-validator/__tests__/parent-ref.test.ts +18 -6
- package/src/engine/boot-validator/__tests__/rate-limit-disabled-reason.test.ts +45 -0
- package/src/engine/boot-validator/access-declarations.ts +3 -5
- package/src/engine/boot-validator/entity-handler.ts +21 -0
- package/src/engine/boot-validator/global-tenancy.ts +13 -1
- package/src/engine/boot-validator/nav.ts +4 -3
- package/src/engine/boot-validator/parent-ref.ts +1 -1
- package/src/engine/boot-validator/projection-list-screens.ts +67 -2
- package/src/engine/boot-validator/query-output-columns.ts +33 -0
- package/src/engine/boot-validator/screens.ts +154 -31
- package/src/engine/build-config-feature-schema.ts +12 -2
- package/src/engine/entity-handlers.ts +72 -20
- 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/feature-entity-handlers.ts +4 -4
- package/src/engine/index.ts +5 -0
- package/src/engine/pattern-library/shared-fields.ts +7 -3
- package/src/engine/registry-validate.ts +12 -4
- package/src/engine/schema-builder.ts +6 -0
- package/src/engine/screen-helpers.ts +52 -0
- package/src/engine/steps/_read-source.ts +12 -0
- package/src/engine/steps/read-find-many.ts +8 -2
- package/src/engine/steps/read-find-one.ts +11 -8
- package/src/engine/steps/unsafe-projection-delete.ts +2 -1
- package/src/engine/steps/unsafe-projection-upsert.ts +2 -1
- package/src/engine/types/index.ts +8 -1
- 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/errors/i18n/de.yaml +14 -0
- package/src/errors/i18n/en.yaml +13 -0
- package/src/errors/reasons.ts +4 -0
- package/src/es-ops/__tests__/runner.integration.test.ts +2 -2
- package/src/i18n/required-surface-keys.ts +6 -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__/job-systemdb.integration.test.ts +5 -4
- package/src/jobs/__tests__/jobs.integration.test.ts +42 -1
- package/src/jobs/job-runner.ts +35 -5
- 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__/escape-hatch-audit.integration.test.ts +254 -0
- package/src/pipeline/__tests__/escape-hatch-report.test.ts +149 -0
- 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__/msp-rebuild.integration.test.ts +28 -0
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +94 -0
- package/src/pipeline/__tests__/projections-runner-systemscope.integration.test.ts +101 -0
- package/src/pipeline/__tests__/query-projection.integration.test.ts +172 -17
- package/src/pipeline/__tests__/rate-limit-default.integration.test.ts +118 -0
- package/src/pipeline/__tests__/rate-limit-default.test.ts +117 -0
- package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +5 -2
- package/src/pipeline/__tests__/system-identity-switch.test.ts +240 -1
- package/src/pipeline/dispatch-query.ts +27 -8
- package/src/pipeline/dispatch-shared.ts +141 -57
- package/src/pipeline/dispatch-stream.ts +1 -3
- package/src/pipeline/dispatch-write.ts +62 -14
- package/src/pipeline/dispatcher.ts +20 -0
- package/src/pipeline/escape-hatch-report.ts +127 -0
- package/src/pipeline/index.ts +8 -0
- package/src/pipeline/member-read-only-transaction.ts +75 -0
- package/src/pipeline/msp-rebuild.ts +2 -0
- package/src/pipeline/projection-rebuild.ts +2 -0
- package/src/pipeline/projections-runner.ts +15 -13
- package/src/pipeline/system-identity-switch.ts +121 -9
- package/src/scripts/codemod/migrate-cross-tenant.ts +335 -0
- package/src/scripts/codemod/migrate-db-raw.ts +19 -6
- package/src/scripts/codemod/migrate-open-to-all.ts +249 -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/src/ui-types/index.ts +3 -0
- package/src/utils/__tests__/ids.test.ts +29 -1
- package/src/utils/ids.ts +10 -1
- package/src/utils/index.ts +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// fw#2914 — r.step.read.findOne/findMany now apply the caller's tenant
|
|
2
|
+
// filter by default (own tenant + SYSTEM_TENANT_ID reference rows), like
|
|
3
|
+
// ctx.db method-form reads. Cross-tenant reads need `unsafeAllTenants:
|
|
4
|
+
// { reason }` on the step AND `escapeHatch: { reason }` on the handler,
|
|
5
|
+
// reported as an "unsafe-raw" escape-hatch audit event. Real HTTP calls +
|
|
6
|
+
// setupTestStack — never createTestDispatcher. Modelled on the M.1.5
|
|
7
|
+
// section of pipeline-handler.integration.test.ts and
|
|
8
|
+
// escape-hatch-audit.integration.test.ts.
|
|
9
|
+
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { table, text, uuid } from "../../db/dialect";
|
|
13
|
+
import { insertOne } from "../../db/query";
|
|
14
|
+
import { defineFeature, defineWriteHandler } from "../../engine";
|
|
15
|
+
import { setupTestStack, type TestStack, TestUsers, unsafePushTables } from "../../stack";
|
|
16
|
+
import { stepsPipeline } from "../pipeline";
|
|
17
|
+
import type { EscapeHatchUseEvent } from "../types";
|
|
18
|
+
|
|
19
|
+
const readFilterTable = table("fw2914_read_filter_items", {
|
|
20
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
21
|
+
tenantId: uuid("tenant_id").notNull(),
|
|
22
|
+
label: text("label").notNull(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const UNDECLARED_REASON =
|
|
26
|
+
"fw#2914 integration test — cross-tenant read step without a handler grant";
|
|
27
|
+
const ESCAPED_REASON = "fw#2914 integration test — cross-tenant widget count for read step";
|
|
28
|
+
|
|
29
|
+
const listSchema = z.object({});
|
|
30
|
+
const listHandler = defineWriteHandler({
|
|
31
|
+
name: "list",
|
|
32
|
+
schema: listSchema,
|
|
33
|
+
access: { roles: ["Admin"] },
|
|
34
|
+
perform: stepsPipeline<z.infer<typeof listSchema>, { count: number; ids: string[] }>(({ r }) => [
|
|
35
|
+
r.step.read.findMany("items", { table: readFilterTable }),
|
|
36
|
+
r.step.return(({ steps }) => {
|
|
37
|
+
const rows = steps["items"] as readonly { id: string }[];
|
|
38
|
+
return {
|
|
39
|
+
isSuccess: true as const,
|
|
40
|
+
data: { count: rows.length, ids: rows.map((row) => row.id) },
|
|
41
|
+
};
|
|
42
|
+
}),
|
|
43
|
+
]),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const getSchema = z.object({ id: z.uuid() });
|
|
47
|
+
const getHandler = defineWriteHandler({
|
|
48
|
+
name: "get",
|
|
49
|
+
schema: getSchema,
|
|
50
|
+
access: { roles: ["Admin"] },
|
|
51
|
+
perform: stepsPipeline<z.infer<typeof getSchema>, { found: boolean; label: string | null }>(
|
|
52
|
+
({ event, r }) => [
|
|
53
|
+
r.step.read.findOne("item", {
|
|
54
|
+
table: readFilterTable,
|
|
55
|
+
where: () => ({ id: event.payload.id }),
|
|
56
|
+
}),
|
|
57
|
+
r.step.return(({ steps }) => {
|
|
58
|
+
const row = steps["item"] as { label?: string } | null;
|
|
59
|
+
return {
|
|
60
|
+
isSuccess: true as const,
|
|
61
|
+
data: { found: row !== null, label: row?.label ?? null },
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
64
|
+
],
|
|
65
|
+
),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const listUnsafeUndeclaredSchema = z.object({});
|
|
69
|
+
const listUnsafeUndeclaredHandler = defineWriteHandler({
|
|
70
|
+
name: "list-unsafe-undeclared",
|
|
71
|
+
schema: listUnsafeUndeclaredSchema,
|
|
72
|
+
access: { roles: ["Admin"] },
|
|
73
|
+
perform: stepsPipeline<z.infer<typeof listUnsafeUndeclaredSchema>, { count: number }>(({ r }) => [
|
|
74
|
+
r.step.read.findMany("items", {
|
|
75
|
+
table: readFilterTable,
|
|
76
|
+
unsafeAllTenants: { reason: UNDECLARED_REASON },
|
|
77
|
+
}),
|
|
78
|
+
r.step.return(({ steps }) => ({
|
|
79
|
+
isSuccess: true as const,
|
|
80
|
+
data: { count: (steps["items"] as readonly unknown[]).length },
|
|
81
|
+
})),
|
|
82
|
+
]),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const listEscapedSchema = z.object({});
|
|
86
|
+
const listEscapedHandler = defineWriteHandler({
|
|
87
|
+
name: "list-escaped",
|
|
88
|
+
schema: listEscapedSchema,
|
|
89
|
+
access: { roles: ["Admin"] },
|
|
90
|
+
escapeHatch: { reason: ESCAPED_REASON },
|
|
91
|
+
perform: stepsPipeline<z.infer<typeof listEscapedSchema>, { count: number }>(({ r }) => [
|
|
92
|
+
r.step.read.findMany("items", {
|
|
93
|
+
table: readFilterTable,
|
|
94
|
+
unsafeAllTenants: { reason: ESCAPED_REASON },
|
|
95
|
+
}),
|
|
96
|
+
r.step.return(({ steps }) => ({
|
|
97
|
+
isSuccess: true as const,
|
|
98
|
+
data: { count: (steps["items"] as readonly unknown[]).length },
|
|
99
|
+
})),
|
|
100
|
+
]),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const readStepsTenantFilterFeature = defineFeature("read-steps-tenant-filter", (r) => {
|
|
104
|
+
r.requires.projection("fw2914_read_filter_items");
|
|
105
|
+
r.writeHandler(listHandler);
|
|
106
|
+
r.writeHandler(getHandler);
|
|
107
|
+
r.writeHandler(listUnsafeUndeclaredHandler);
|
|
108
|
+
r.writeHandler(listEscapedHandler);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const admin = TestUsers.admin;
|
|
112
|
+
const otherTenant = TestUsers.otherTenant;
|
|
113
|
+
|
|
114
|
+
describe("r.step.read.findOne/findMany tenant-filtering (fw#2914)", () => {
|
|
115
|
+
let stack: TestStack;
|
|
116
|
+
const events: EscapeHatchUseEvent[] = [];
|
|
117
|
+
let adminRowId: string;
|
|
118
|
+
let otherTenantRowId: string;
|
|
119
|
+
|
|
120
|
+
beforeAll(async () => {
|
|
121
|
+
stack = await setupTestStack({
|
|
122
|
+
features: [readStepsTenantFilterFeature],
|
|
123
|
+
extraContext: {
|
|
124
|
+
_escapeHatchAuditSink: async (event: EscapeHatchUseEvent) => {
|
|
125
|
+
events.push(event);
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
await unsafePushTables(stack.db, { fw2914_read_filter_items: readFilterTable });
|
|
130
|
+
|
|
131
|
+
const adminRow = await insertOne<{ id: string }>(stack.db, readFilterTable, {
|
|
132
|
+
tenantId: admin.tenantId,
|
|
133
|
+
label: "admin-row",
|
|
134
|
+
});
|
|
135
|
+
const otherRow = await insertOne<{ id: string }>(stack.db, readFilterTable, {
|
|
136
|
+
tenantId: otherTenant.tenantId,
|
|
137
|
+
label: "other-tenant-row",
|
|
138
|
+
});
|
|
139
|
+
adminRowId = adminRow!.id;
|
|
140
|
+
otherTenantRowId = otherRow!.id;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
afterAll(async () => {
|
|
144
|
+
await stack.cleanup();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("read.findMany returns only the caller's own tenant rows", async () => {
|
|
148
|
+
const data = await stack.http.writeOk<{ count: number; ids: string[] }>(
|
|
149
|
+
"read-steps-tenant-filter:write:list",
|
|
150
|
+
{},
|
|
151
|
+
admin,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(data.count).toBe(1);
|
|
155
|
+
expect(data.ids).toEqual([adminRowId]);
|
|
156
|
+
expect(data.ids).not.toContain(otherTenantRowId);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("read.findOne returns the row when it belongs to the caller's tenant", async () => {
|
|
160
|
+
const data = await stack.http.writeOk<{ found: boolean; label: string | null }>(
|
|
161
|
+
"read-steps-tenant-filter:write:get",
|
|
162
|
+
{ id: adminRowId },
|
|
163
|
+
admin,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(data).toEqual({ found: true, label: "admin-row" });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("read.findOne returns null for a row belonging to a different tenant", async () => {
|
|
170
|
+
const data = await stack.http.writeOk<{ found: boolean; label: string | null }>(
|
|
171
|
+
"read-steps-tenant-filter:write:get",
|
|
172
|
+
{ id: otherTenantRowId },
|
|
173
|
+
admin,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(data).toEqual({ found: false, label: null });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("unsafeAllTenants without escapeHatch on the handler is denied and reports nothing", async () => {
|
|
180
|
+
events.length = 0;
|
|
181
|
+
|
|
182
|
+
const err = await stack.http.writeErr(
|
|
183
|
+
"read-steps-tenant-filter:write:list-unsafe-undeclared",
|
|
184
|
+
{},
|
|
185
|
+
admin,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(err.httpStatus).toBe(403);
|
|
189
|
+
expect(err.code).toBe("access_denied");
|
|
190
|
+
expect(events.filter((e) => e.kind === "unsafe-raw")).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("unsafeAllTenants with escapeHatch on the handler sees rows across tenants and reports unsafe-raw", async () => {
|
|
194
|
+
events.length = 0;
|
|
195
|
+
|
|
196
|
+
const data = await stack.http.writeOk<{ count: number }>(
|
|
197
|
+
"read-steps-tenant-filter:write:list-escaped",
|
|
198
|
+
{},
|
|
199
|
+
admin,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(data.count).toBeGreaterThanOrEqual(2);
|
|
203
|
+
|
|
204
|
+
const matches = events.filter((e) => e.kind === "unsafe-raw");
|
|
205
|
+
expect(matches).toEqual([
|
|
206
|
+
{
|
|
207
|
+
handler: "read-steps-tenant-filter:write:list-escaped",
|
|
208
|
+
kind: "unsafe-raw",
|
|
209
|
+
reason: ESCAPED_REASON,
|
|
210
|
+
tenantId: admin.tenantId,
|
|
211
|
+
actor: admin.id,
|
|
212
|
+
},
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -78,13 +78,17 @@ describe("getAllQueryHandlers", () => {
|
|
|
78
78
|
const taskFeature = defineFeature("registry-test-task", (r) => {
|
|
79
79
|
r.crud("task", taskEntity, {
|
|
80
80
|
write: { access: { roles: ["Admin"] } },
|
|
81
|
-
read: {
|
|
81
|
+
read: {
|
|
82
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
83
|
+
},
|
|
82
84
|
});
|
|
83
85
|
});
|
|
84
86
|
const noteFeature = defineFeature("registry-test-note", (r) => {
|
|
85
87
|
r.crud("note", noteEntity, {
|
|
86
88
|
write: { access: { roles: ["Admin"] } },
|
|
87
|
-
read: {
|
|
89
|
+
read: {
|
|
90
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
91
|
+
},
|
|
88
92
|
});
|
|
89
93
|
});
|
|
90
94
|
|
|
@@ -109,12 +113,12 @@ describe("getAllStreamHandlers", () => {
|
|
|
109
113
|
test("returns every registered stream handler, qualified, across multiple features", () => {
|
|
110
114
|
const aiFeature = defineFeature("registry-test-ai", (r) => {
|
|
111
115
|
r.streamHandler("chat:complete", z.object({ prompt: z.string() }), async function* () {}, {
|
|
112
|
-
access: { openToAll:
|
|
116
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
113
117
|
});
|
|
114
118
|
});
|
|
115
119
|
const otherFeature = defineFeature("registry-test-other", (r) => {
|
|
116
120
|
r.streamHandler("chat:complete", z.object({ prompt: z.string() }), async function* () {}, {
|
|
117
|
-
access: { openToAll:
|
|
121
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
118
122
|
});
|
|
119
123
|
});
|
|
120
124
|
|
|
@@ -137,12 +141,12 @@ describe("getAllStreamHandlers", () => {
|
|
|
137
141
|
test("duplicate stream-handler short-name across features qualifies independently, no collision", () => {
|
|
138
142
|
const aiFeature = defineFeature("registry-test-dup-a", (r) => {
|
|
139
143
|
r.streamHandler("chat:complete", z.object({}), async function* () {}, {
|
|
140
|
-
access: { openToAll:
|
|
144
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
141
145
|
});
|
|
142
146
|
});
|
|
143
147
|
const otherFeature = defineFeature("registry-test-dup-b", (r) => {
|
|
144
148
|
r.streamHandler("chat:complete", z.object({}), async function* () {}, {
|
|
145
|
-
access: { openToAll:
|
|
149
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
146
150
|
});
|
|
147
151
|
});
|
|
148
152
|
expect(() => createRegistry([aiFeature, otherFeature])).not.toThrow();
|
|
@@ -154,12 +158,12 @@ describe("getAllStreamHandlers", () => {
|
|
|
154
158
|
// fire) but toKebab() collapses both to the same qualified name.
|
|
155
159
|
const featureA = defineFeature("registry-test-kebab-dup", (r) => {
|
|
156
160
|
r.streamHandler("chat:complete", z.object({}), async function* () {}, {
|
|
157
|
-
access: { openToAll:
|
|
161
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
158
162
|
});
|
|
159
163
|
});
|
|
160
164
|
const featureB = defineFeature("registryTestKebabDup", (r) => {
|
|
161
165
|
r.streamHandler("chat:complete", z.object({}), async function* () {}, {
|
|
162
|
-
access: { openToAll:
|
|
166
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
163
167
|
});
|
|
164
168
|
});
|
|
165
169
|
expect(() => createRegistry([featureA, featureB])).toThrow(/Duplicate stream handler/);
|
|
@@ -173,7 +177,7 @@ describe("getAllStreamHandlers", () => {
|
|
|
173
177
|
name: "chat:complete",
|
|
174
178
|
schema,
|
|
175
179
|
handler: handlerFn,
|
|
176
|
-
access: { openToAll:
|
|
180
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
177
181
|
rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 60 },
|
|
178
182
|
});
|
|
179
183
|
});
|
|
@@ -184,7 +188,9 @@ describe("getAllStreamHandlers", () => {
|
|
|
184
188
|
expect(registered).toBeDefined();
|
|
185
189
|
expect(registered?.schema).toBe(schema);
|
|
186
190
|
expect(registered?.handler).toBe(handlerFn);
|
|
187
|
-
expect(registered?.access).toEqual({
|
|
191
|
+
expect(registered?.access).toEqual({
|
|
192
|
+
openToAll: { reason: "test handler callable by any signed-in test user" },
|
|
193
|
+
});
|
|
188
194
|
expect(registered?.rateLimit).toEqual({ per: "ip+handler", limit: 5, windowSeconds: 60 });
|
|
189
195
|
});
|
|
190
196
|
});
|
|
@@ -196,7 +202,7 @@ describe("double-qualified handler names (#1991)", () => {
|
|
|
196
202
|
"registry-test-ai-orch:query:duplicate-candidates",
|
|
197
203
|
z.object({}),
|
|
198
204
|
async () => ({}),
|
|
199
|
-
{ access: { openToAll:
|
|
205
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
200
206
|
);
|
|
201
207
|
});
|
|
202
208
|
|
|
@@ -206,7 +212,7 @@ describe("double-qualified handler names (#1991)", () => {
|
|
|
206
212
|
test("createRegistry allows a sub-structured short name whose entity prefix merely resembles the feature name", () => {
|
|
207
213
|
const feature = defineFeature("registry-test-invoices", (r) => {
|
|
208
214
|
r.queryHandler("invoice:mark-paid", z.object({}), async () => ({}), {
|
|
209
|
-
access: { openToAll:
|
|
215
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
210
216
|
});
|
|
211
217
|
});
|
|
212
218
|
|
|
@@ -309,7 +315,7 @@ describe("extensionSelector boot-validation", () => {
|
|
|
309
315
|
"doSomething",
|
|
310
316
|
z.object({}),
|
|
311
317
|
async () => ({ isSuccess: true as const, data: {} }),
|
|
312
|
-
{ access: { openToAll:
|
|
318
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
313
319
|
);
|
|
314
320
|
r.useExtension("credit-cap", "credit");
|
|
315
321
|
});
|
|
@@ -44,6 +44,22 @@ describe("requiredKeysFromScreen", () => {
|
|
|
44
44
|
expect(keys).toContain(fieldLabelKey("publicstatus", ACTION_FORM_ENTITY, "title"));
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
test("actionForm honors a fieldLabels override, falls back to ACTION_FORM_ENTITY otherwise", () => {
|
|
48
|
+
const keys = requiredKeysFromScreen("publicstatus", {
|
|
49
|
+
id: "reschedule-form",
|
|
50
|
+
type: "actionForm",
|
|
51
|
+
handler: "publicstatus:write:incident:reschedule",
|
|
52
|
+
fields: { dueAt: { type: "date" }, note: { type: "text" } },
|
|
53
|
+
fieldLabels: { dueAt: "publicstatus:override.dueAt" },
|
|
54
|
+
layout: {
|
|
55
|
+
sections: [{ fields: ["dueAt", "note"] }],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
expect(keys).toContain("publicstatus:override.dueAt");
|
|
59
|
+
expect(keys).not.toContain(fieldLabelKey("publicstatus", ACTION_FORM_ENTITY, "dueAt"));
|
|
60
|
+
expect(keys).toContain(fieldLabelKey("publicstatus", ACTION_FORM_ENTITY, "note"));
|
|
61
|
+
});
|
|
62
|
+
|
|
47
63
|
test("entityEdit emits submitLabel + section titles + field labels (override honored)", () => {
|
|
48
64
|
const screen: EntityEditScreenDefinition = {
|
|
49
65
|
id: "component-edit",
|
|
@@ -4,6 +4,7 @@ import { withBootValidatorFixture } from "../../testing/boot-validator-fixture";
|
|
|
4
4
|
import { validateBoot as validateBootRaw } from "../boot-validator";
|
|
5
5
|
import { createTenantConfig } from "../config-helpers";
|
|
6
6
|
import { defineFeature } from "../define-feature";
|
|
7
|
+
import { defineEntityListHandler } from "../entity-handlers";
|
|
7
8
|
import { createDerivedField, createEntity, createTextField } from "../factories";
|
|
8
9
|
import { createRegistry } from "../registry";
|
|
9
10
|
import type { ScreenDefinition } from "../types/screen";
|
|
@@ -211,7 +212,7 @@ describe("r.screen() — registration", () => {
|
|
|
211
212
|
const features = [
|
|
212
213
|
defineFeature("app", (r) => {
|
|
213
214
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
214
|
-
access: { openToAll:
|
|
215
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
215
216
|
});
|
|
216
217
|
r.screen({
|
|
217
218
|
id: "x",
|
|
@@ -281,10 +282,10 @@ describe("r.screen() — registration", () => {
|
|
|
281
282
|
const features = [
|
|
282
283
|
defineFeature("app", (r) => {
|
|
283
284
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
284
|
-
access: { openToAll:
|
|
285
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
285
286
|
});
|
|
286
287
|
r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
287
|
-
access: { openToAll:
|
|
288
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
288
289
|
});
|
|
289
290
|
r.screen({
|
|
290
291
|
id: "x",
|
|
@@ -345,10 +346,10 @@ describe("r.screen() — registration", () => {
|
|
|
345
346
|
const features = [
|
|
346
347
|
defineFeature("app", (r) => {
|
|
347
348
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
348
|
-
access: { openToAll:
|
|
349
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
349
350
|
});
|
|
350
351
|
r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
351
|
-
access: { openToAll:
|
|
352
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
352
353
|
});
|
|
353
354
|
r.screen({
|
|
354
355
|
id: "x",
|
|
@@ -370,10 +371,10 @@ describe("r.screen() — registration", () => {
|
|
|
370
371
|
const features = [
|
|
371
372
|
defineFeature("app", (r) => {
|
|
372
373
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
373
|
-
access: { openToAll:
|
|
374
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
374
375
|
});
|
|
375
376
|
r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
376
|
-
access: { openToAll:
|
|
377
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
377
378
|
});
|
|
378
379
|
r.screen({
|
|
379
380
|
id: "x",
|
|
@@ -400,13 +401,15 @@ describe("r.screen() — registration", () => {
|
|
|
400
401
|
const features = [
|
|
401
402
|
defineFeature("app", (r) => {
|
|
402
403
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
403
|
-
access: { openToAll:
|
|
404
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
404
405
|
});
|
|
405
406
|
r.queryHandler(
|
|
406
407
|
"foo:list",
|
|
407
408
|
z.object({ search: z.string().optional() }),
|
|
408
409
|
async () => ({ rows: [], nextCursor: null }),
|
|
409
|
-
{
|
|
410
|
+
{
|
|
411
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
412
|
+
},
|
|
410
413
|
);
|
|
411
414
|
r.screen({
|
|
412
415
|
id: "x",
|
|
@@ -433,10 +436,10 @@ describe("r.screen() — registration", () => {
|
|
|
433
436
|
const features = [
|
|
434
437
|
defineFeature("app", (r) => {
|
|
435
438
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
436
|
-
access: { openToAll:
|
|
439
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
437
440
|
});
|
|
438
441
|
r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
439
|
-
access: { openToAll:
|
|
442
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
440
443
|
});
|
|
441
444
|
r.screen({
|
|
442
445
|
id: "x",
|
|
@@ -470,13 +473,15 @@ describe("r.screen() — registration", () => {
|
|
|
470
473
|
const features = [
|
|
471
474
|
defineFeature("app", (r) => {
|
|
472
475
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
473
|
-
access: { openToAll:
|
|
476
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
474
477
|
});
|
|
475
478
|
r.queryHandler(
|
|
476
479
|
"foo:list",
|
|
477
480
|
z.object({ filters: z.array(z.object({})).optional() }),
|
|
478
481
|
async () => ({ rows: [], nextCursor: null }),
|
|
479
|
-
{
|
|
482
|
+
{
|
|
483
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
484
|
+
},
|
|
480
485
|
);
|
|
481
486
|
r.screen({
|
|
482
487
|
id: "x",
|
|
@@ -512,13 +517,15 @@ describe("r.screen() — registration", () => {
|
|
|
512
517
|
const features = [
|
|
513
518
|
defineFeature("app", (r) => {
|
|
514
519
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
515
|
-
access: { openToAll:
|
|
520
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
516
521
|
});
|
|
517
522
|
r.queryHandler(
|
|
518
523
|
"foo:list",
|
|
519
524
|
z.object({ filters: z.array(z.object({})).optional() }),
|
|
520
525
|
async () => ({ rows: [], nextCursor: null }),
|
|
521
|
-
{
|
|
526
|
+
{
|
|
527
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
528
|
+
},
|
|
522
529
|
);
|
|
523
530
|
r.screen({
|
|
524
531
|
id: "x",
|
|
@@ -549,6 +556,155 @@ describe("r.screen() — registration", () => {
|
|
|
549
556
|
});
|
|
550
557
|
});
|
|
551
558
|
|
|
559
|
+
describe("relatedList parentFilter", () => {
|
|
560
|
+
test("rejects parentFilter combined with parentParam on the same section", () => {
|
|
561
|
+
const features = [
|
|
562
|
+
defineFeature("app", (r) => {
|
|
563
|
+
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
564
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
565
|
+
});
|
|
566
|
+
r.queryHandler(
|
|
567
|
+
"foo:list",
|
|
568
|
+
z.object({ filter: z.unknown().optional() }),
|
|
569
|
+
async () => ({ rows: [], nextCursor: null }),
|
|
570
|
+
{
|
|
571
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
572
|
+
},
|
|
573
|
+
);
|
|
574
|
+
r.screen({
|
|
575
|
+
id: "x",
|
|
576
|
+
type: "projectionDetail",
|
|
577
|
+
query: "app:query:foo:detail",
|
|
578
|
+
layout: {
|
|
579
|
+
sections: [
|
|
580
|
+
{
|
|
581
|
+
kind: "relatedList",
|
|
582
|
+
title: "s",
|
|
583
|
+
query: "app:query:foo:list",
|
|
584
|
+
columns: ["name"],
|
|
585
|
+
parentParam: "orderId",
|
|
586
|
+
parentFilter: { field: "orderId" },
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
}),
|
|
592
|
+
];
|
|
593
|
+
expect(() => validateBoot(features)).toThrow(/declares both parentFilter and parentParam/);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("rejects parentFilter when the bound query's schema has no filter parameter", () => {
|
|
597
|
+
const features = [
|
|
598
|
+
defineFeature("app", (r) => {
|
|
599
|
+
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
600
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
601
|
+
});
|
|
602
|
+
r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
603
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
604
|
+
});
|
|
605
|
+
r.screen({
|
|
606
|
+
id: "x",
|
|
607
|
+
type: "projectionDetail",
|
|
608
|
+
query: "app:query:foo:detail",
|
|
609
|
+
layout: {
|
|
610
|
+
sections: [
|
|
611
|
+
{
|
|
612
|
+
kind: "relatedList",
|
|
613
|
+
title: "s",
|
|
614
|
+
query: "app:query:foo:list",
|
|
615
|
+
columns: ["name"],
|
|
616
|
+
parentFilter: { field: "orderId" },
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
}),
|
|
622
|
+
];
|
|
623
|
+
expect(() => validateBoot(features)).toThrow(
|
|
624
|
+
/declares parentFilter but query.*no "filter" parameter/,
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("rejects parentFilter.field that is not a declared field on the entity behind the query", () => {
|
|
629
|
+
const orderEntity = createEntity({
|
|
630
|
+
table: "orders",
|
|
631
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
632
|
+
});
|
|
633
|
+
const features = [
|
|
634
|
+
defineFeature("app", (r) => {
|
|
635
|
+
r.entity("order", orderEntity);
|
|
636
|
+
r.queryHandler(
|
|
637
|
+
defineEntityListHandler("order", orderEntity, {
|
|
638
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
641
|
+
r.screen({
|
|
642
|
+
id: "x",
|
|
643
|
+
type: "projectionDetail",
|
|
644
|
+
query: "app:query:foo:detail",
|
|
645
|
+
layout: {
|
|
646
|
+
sections: [
|
|
647
|
+
{
|
|
648
|
+
kind: "relatedList",
|
|
649
|
+
title: "s",
|
|
650
|
+
query: "app:query:order:list",
|
|
651
|
+
columns: ["name"],
|
|
652
|
+
parentFilter: { field: "unknownField" },
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
658
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
659
|
+
});
|
|
660
|
+
}),
|
|
661
|
+
];
|
|
662
|
+
expect(() => validateBoot(features)).toThrow(
|
|
663
|
+
/parentFilter\.field "unknownField" is not a declared field/,
|
|
664
|
+
);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("accepts a valid parentFilter whose field is a real entity field and the query accepts filter", () => {
|
|
668
|
+
const orderEntity = createEntity({
|
|
669
|
+
table: "orders",
|
|
670
|
+
fields: {
|
|
671
|
+
name: createTextField({ personal: false, reason: "test_fixture" }),
|
|
672
|
+
orderId: createTextField({ personal: false, reason: "test_fixture" }),
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
const features = [
|
|
676
|
+
defineFeature("app", (r) => {
|
|
677
|
+
r.entity("order", orderEntity);
|
|
678
|
+
r.queryHandler(
|
|
679
|
+
defineEntityListHandler("order", orderEntity, {
|
|
680
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
681
|
+
}),
|
|
682
|
+
);
|
|
683
|
+
r.screen({
|
|
684
|
+
id: "x",
|
|
685
|
+
type: "projectionDetail",
|
|
686
|
+
query: "app:query:foo:detail",
|
|
687
|
+
layout: {
|
|
688
|
+
sections: [
|
|
689
|
+
{
|
|
690
|
+
kind: "relatedList",
|
|
691
|
+
title: "s",
|
|
692
|
+
query: "app:query:order:list",
|
|
693
|
+
columns: ["name"],
|
|
694
|
+
parentFilter: { field: "orderId" },
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
700
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
701
|
+
});
|
|
702
|
+
}),
|
|
703
|
+
];
|
|
704
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
552
708
|
test("validateBoot rejects a relatedList section on entityEdit (fw#2166)", () => {
|
|
553
709
|
const features = [
|
|
554
710
|
defineFeature("shop", (r) => {
|
|
@@ -660,10 +816,10 @@ describe("r.screen() — registration", () => {
|
|
|
660
816
|
},
|
|
661
817
|
);
|
|
662
818
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
663
|
-
access: { openToAll:
|
|
819
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
664
820
|
});
|
|
665
821
|
r.queryHandler("foo:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
666
|
-
access: { openToAll:
|
|
822
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
667
823
|
});
|
|
668
824
|
r.screen({
|
|
669
825
|
id: "x",
|
|
@@ -873,7 +1029,7 @@ describe("r.screen() — registration", () => {
|
|
|
873
1029
|
const features = [
|
|
874
1030
|
defineFeature("app", (r) => {
|
|
875
1031
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
876
|
-
access: { openToAll:
|
|
1032
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
877
1033
|
});
|
|
878
1034
|
r.writeHandler(
|
|
879
1035
|
"save",
|
|
@@ -917,7 +1073,7 @@ describe("r.screen() — registration", () => {
|
|
|
917
1073
|
// here so the throw below isolates the one key this test is about.
|
|
918
1074
|
r.translations({ keys: { "screen:x.title": { en: "X" } } });
|
|
919
1075
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
920
|
-
access: { openToAll:
|
|
1076
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
921
1077
|
});
|
|
922
1078
|
r.writeHandler(
|
|
923
1079
|
"save",
|
|
@@ -959,7 +1115,7 @@ describe("r.screen() — registration", () => {
|
|
|
959
1115
|
},
|
|
960
1116
|
});
|
|
961
1117
|
r.queryHandler("foo:detail", z.object({}), async () => ({}), {
|
|
962
|
-
access: { openToAll:
|
|
1118
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
963
1119
|
});
|
|
964
1120
|
r.writeHandler(
|
|
965
1121
|
"save",
|
|
@@ -58,7 +58,6 @@ function makeCtx(opts: { graceDays?: number; calls: DeleteCall[] }): JobContext
|
|
|
58
58
|
// to this recorder instead of trying to extract real table metadata.
|
|
59
59
|
const fakeDb = {
|
|
60
60
|
tenantId: "t1",
|
|
61
|
-
raw: { unsafe: async () => [] },
|
|
62
61
|
selectMany: async () => [],
|
|
63
62
|
fetchOne: async () => undefined,
|
|
64
63
|
insertOne: async () => undefined,
|