@cosmicdrift/kumiko-framework 0.259.0 → 0.261.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/package.json +3 -3
- package/src/changes.json +13 -0
- package/src/db/__tests__/tenant-db-global.integration.test.ts +97 -0
- package/src/db/__tests__/tenant-db-global.test.ts +170 -0
- package/src/db/entity-table-meta.ts +8 -2
- package/src/db/table-builder.ts +37 -2
- package/src/db/tenant-db.ts +86 -4
- package/src/engine/__tests__/boot-validator.test.ts +41 -16
- package/src/engine/__tests__/build-app-schema.test.ts +14 -5
- package/src/engine/__tests__/engine.test.ts +10 -0
- package/src/engine/__tests__/entity-handlers.test.ts +48 -36
- package/src/engine/__tests__/extends-registrar.test.ts +27 -12
- package/src/engine/__tests__/projection-detail-tabs.test.ts +43 -0
- package/src/engine/__tests__/registry.test.ts +16 -5
- package/src/engine/__tests__/screen.test.ts +20 -0
- package/src/engine/access.ts +2 -1
- package/src/engine/boot-validator/__tests__/access-declarations.test.ts +200 -0
- package/src/engine/boot-validator/access-declarations.ts +141 -0
- package/src/engine/boot-validator/entity-handler.ts +1 -1
- package/src/engine/boot-validator/index.ts +2 -0
- package/src/engine/boot-validator/screens.ts +53 -38
- package/src/engine/define-handler.ts +1 -0
- package/src/engine/entity-handlers.ts +29 -19
- package/src/engine/factories.ts +5 -3
- package/src/engine/feature-ast/extractors/hooks.ts +5 -1
- package/src/engine/feature-entity-handlers.ts +40 -27
- package/src/engine/index.ts +4 -0
- package/src/engine/types/handlers.ts +1 -0
- package/src/engine/types/index.ts +4 -0
- package/src/pipeline/__tests__/escape-hatch.integration.test.ts +93 -0
- package/src/pipeline/dispatch-shared.ts +3 -0
- package/src/secrets/__tests__/contains-secret.test.ts +5 -0
- package/src/testing/__tests__/e2e-generator.test.ts +11 -5
- package/src/ui-types/index.ts +6 -1
|
@@ -42,8 +42,12 @@ export function readOptionalAccessRule(value: unknown): AccessRule | undefined {
|
|
|
42
42
|
if (Array.isArray(value["roles"]) && value["roles"].every((r) => typeof r === "string")) {
|
|
43
43
|
return { roles: value["roles"] as readonly string[] };
|
|
44
44
|
}
|
|
45
|
+
const publicIntake = value["publicIntake"] === true ? { publicIntake: true as const } : {};
|
|
45
46
|
if (value["openToAll"] === true) {
|
|
46
|
-
return { openToAll: true };
|
|
47
|
+
return { openToAll: true, ...publicIntake };
|
|
48
|
+
}
|
|
49
|
+
if (isPlainObject(value["openToAll"]) && typeof value["openToAll"]["reason"] === "string") {
|
|
50
|
+
return { openToAll: { reason: value["openToAll"]["reason"] }, ...publicIntake };
|
|
47
51
|
}
|
|
48
52
|
return undefined;
|
|
49
53
|
}
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
AgentHandlerHints,
|
|
20
20
|
EntityDefinition,
|
|
21
21
|
EntityRef,
|
|
22
|
+
EscapeHatchDeclaration,
|
|
22
23
|
HandlerRef,
|
|
23
24
|
NameOrRef,
|
|
24
25
|
QueryHandlerFn,
|
|
@@ -67,6 +68,18 @@ function agentSlots(source: {
|
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function requireInlineHandlerArgs<TSchema, THandler, TOptions extends object>(
|
|
72
|
+
kind: string,
|
|
73
|
+
schema: TSchema | undefined,
|
|
74
|
+
handler: THandler | undefined,
|
|
75
|
+
options: TOptions | undefined,
|
|
76
|
+
): { schema: TSchema; handler: THandler; options: TOptions } {
|
|
77
|
+
if (!schema || !handler || !options) {
|
|
78
|
+
throw new Error(`${kind} inline form requires schema + handler + options.access`);
|
|
79
|
+
}
|
|
80
|
+
return { schema, handler, options };
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
// Builds entity/relation/writeHandler/queryHandler — the registrar
|
|
71
84
|
// methods that create or reference entities.
|
|
72
85
|
export function buildEntityHandlerMethods<TName extends string>(
|
|
@@ -110,10 +123,11 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
110
123
|
schema?: TSchema,
|
|
111
124
|
handler?: WriteHandlerFn<z.infer<TSchema>>,
|
|
112
125
|
options?: {
|
|
113
|
-
access
|
|
126
|
+
access: AccessRule;
|
|
114
127
|
rateLimit?: RateLimitOption;
|
|
115
128
|
description?: string;
|
|
116
129
|
agent?: AgentHandlerHints;
|
|
130
|
+
escapeHatch?: EscapeHatchDeclaration;
|
|
117
131
|
},
|
|
118
132
|
): HandlerRef {
|
|
119
133
|
if (typeof nameOrDef === "object") {
|
|
@@ -130,10 +144,11 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
130
144
|
// `satisfies` does not work here (it asserts assignability, which
|
|
131
145
|
// is what fails). Explicit cast is the right tool.
|
|
132
146
|
handler: def.handler as WriteHandlerFn,
|
|
133
|
-
|
|
147
|
+
access: def.access,
|
|
134
148
|
...agentSlots(def),
|
|
135
149
|
...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
|
|
136
150
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
151
|
+
...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
|
|
137
152
|
// Forward the pipeline-build closure so boot-validators and
|
|
138
153
|
// Designer/AI tooling can inspect the step list. Absent on
|
|
139
154
|
// free-form handlers — defineWriteHandler only sets `perform`
|
|
@@ -147,15 +162,15 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
147
162
|
tryMapEntity(state, name, def.name);
|
|
148
163
|
return { name: def.name };
|
|
149
164
|
}
|
|
150
|
-
|
|
151
|
-
throw new Error("writeHandler inline form requires schema + handler");
|
|
165
|
+
const inline = requireInlineHandlerArgs("writeHandler", schema, handler, options);
|
|
152
166
|
state.writeHandlers[nameOrDef] = {
|
|
153
167
|
name: nameOrDef,
|
|
154
|
-
schema,
|
|
155
|
-
handler: handler as WriteHandlerFn, // @cast-boundary engine-bridge
|
|
156
|
-
|
|
157
|
-
...agentSlots(options
|
|
158
|
-
...(options
|
|
168
|
+
schema: inline.schema,
|
|
169
|
+
handler: inline.handler as WriteHandlerFn, // @cast-boundary engine-bridge
|
|
170
|
+
access: inline.options.access,
|
|
171
|
+
...agentSlots(inline.options),
|
|
172
|
+
...(inline.options.rateLimit && { rateLimit: inline.options.rateLimit }),
|
|
173
|
+
...(inline.options.escapeHatch && { escapeHatch: inline.options.escapeHatch }),
|
|
159
174
|
};
|
|
160
175
|
tryMapEntity(state, name, nameOrDef);
|
|
161
176
|
return { name: nameOrDef };
|
|
@@ -165,7 +180,7 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
165
180
|
schema?: TSchema,
|
|
166
181
|
handler?: QueryHandlerFn<z.infer<TSchema>>,
|
|
167
182
|
options?: {
|
|
168
|
-
access
|
|
183
|
+
access: AccessRule;
|
|
169
184
|
rateLimit?: RateLimitOption;
|
|
170
185
|
outputSchema?: ZodType;
|
|
171
186
|
description?: string;
|
|
@@ -179,7 +194,7 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
179
194
|
schema: def.schema,
|
|
180
195
|
// @cast-boundary engine-bridge — typed Dev-API → erased internal storage
|
|
181
196
|
handler: def.handler as QueryHandlerFn, // @cast-boundary engine-bridge
|
|
182
|
-
|
|
197
|
+
access: def.access,
|
|
183
198
|
...agentSlots(def),
|
|
184
199
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
185
200
|
...(def.outputSchema && { outputSchema: def.outputSchema }),
|
|
@@ -193,16 +208,15 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
193
208
|
tryMapEntity(state, name, def.name);
|
|
194
209
|
return { name: def.name };
|
|
195
210
|
}
|
|
196
|
-
|
|
197
|
-
throw new Error("queryHandler inline form requires schema + handler");
|
|
211
|
+
const inline = requireInlineHandlerArgs("queryHandler", schema, handler, options);
|
|
198
212
|
state.queryHandlers[nameOrDef] = {
|
|
199
213
|
name: nameOrDef,
|
|
200
|
-
schema,
|
|
201
|
-
handler: handler as QueryHandlerFn, // @cast-boundary engine-bridge
|
|
202
|
-
|
|
203
|
-
...agentSlots(options
|
|
204
|
-
...(options
|
|
205
|
-
...(options
|
|
214
|
+
schema: inline.schema,
|
|
215
|
+
handler: inline.handler as QueryHandlerFn, // @cast-boundary engine-bridge
|
|
216
|
+
access: inline.options.access,
|
|
217
|
+
...agentSlots(inline.options),
|
|
218
|
+
...(inline.options.rateLimit && { rateLimit: inline.options.rateLimit }),
|
|
219
|
+
...(inline.options.outputSchema && { outputSchema: inline.options.outputSchema }),
|
|
206
220
|
};
|
|
207
221
|
tryMapEntity(state, name, nameOrDef);
|
|
208
222
|
return { name: nameOrDef };
|
|
@@ -211,7 +225,7 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
211
225
|
nameOrDef: string | StreamHandlerDefinition<TName, TSchema>,
|
|
212
226
|
schema?: TSchema,
|
|
213
227
|
handler?: StreamHandlerFn<z.infer<TSchema>>,
|
|
214
|
-
options?: { access
|
|
228
|
+
options?: { access: AccessRule; rateLimit?: RateLimitOption },
|
|
215
229
|
): HandlerRef {
|
|
216
230
|
if (typeof nameOrDef === "object") {
|
|
217
231
|
const def = nameOrDef;
|
|
@@ -220,19 +234,18 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
220
234
|
schema: def.schema,
|
|
221
235
|
// @cast-boundary engine-bridge — typed Dev-API → erased internal storage
|
|
222
236
|
handler: def.handler as StreamHandlerFn, // @cast-boundary engine-bridge
|
|
223
|
-
|
|
237
|
+
access: def.access,
|
|
224
238
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
225
239
|
};
|
|
226
240
|
return { name: def.name };
|
|
227
241
|
}
|
|
228
|
-
|
|
229
|
-
throw new Error("streamHandler inline form requires schema + handler");
|
|
242
|
+
const inline = requireInlineHandlerArgs("streamHandler", schema, handler, options);
|
|
230
243
|
state.streamHandlers[nameOrDef] = {
|
|
231
244
|
name: nameOrDef,
|
|
232
|
-
schema,
|
|
233
|
-
handler: handler as StreamHandlerFn, // @cast-boundary engine-bridge
|
|
234
|
-
|
|
235
|
-
...(options
|
|
245
|
+
schema: inline.schema,
|
|
246
|
+
handler: inline.handler as StreamHandlerFn, // @cast-boundary engine-bridge
|
|
247
|
+
access: inline.options.access,
|
|
248
|
+
...(inline.options.rateLimit && { rateLimit: inline.options.rateLimit }),
|
|
236
249
|
};
|
|
237
250
|
return { name: nameOrDef };
|
|
238
251
|
},
|
package/src/engine/index.ts
CHANGED
|
@@ -358,6 +358,7 @@ export type {
|
|
|
358
358
|
EntityListScreenDefinition,
|
|
359
359
|
EntityRef,
|
|
360
360
|
EntityRelations,
|
|
361
|
+
EscapeHatchDeclaration,
|
|
361
362
|
EventDef,
|
|
362
363
|
FeatureDefinition,
|
|
363
364
|
FeatureRegistrar,
|
|
@@ -414,6 +415,8 @@ export type {
|
|
|
414
415
|
NotifyPriority,
|
|
415
416
|
NumberFieldDef,
|
|
416
417
|
OnDeleteStrategy,
|
|
418
|
+
OpenToAllAccessRule,
|
|
419
|
+
OpenToAllDeclaration,
|
|
417
420
|
ParentRefDef,
|
|
418
421
|
PersonalAnnotations,
|
|
419
422
|
PersonalAnnotationsLongText,
|
|
@@ -489,6 +492,7 @@ export {
|
|
|
489
492
|
DEFAULT_LOCALES,
|
|
490
493
|
HookPhases,
|
|
491
494
|
isAgentVisibleScreen,
|
|
495
|
+
isOpenToAllGranted,
|
|
492
496
|
resolveAgentExposure,
|
|
493
497
|
} from "./types";
|
|
494
498
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers";
|
|
@@ -156,6 +156,7 @@ export type {
|
|
|
156
156
|
DeclarativeEventMigration,
|
|
157
157
|
DispatchWriteRef,
|
|
158
158
|
EntityRef,
|
|
159
|
+
EscapeHatchDeclaration,
|
|
159
160
|
EventDef,
|
|
160
161
|
EventMigrationDef,
|
|
161
162
|
EventPiiFields,
|
|
@@ -172,6 +173,8 @@ export type {
|
|
|
172
173
|
NotifyFn,
|
|
173
174
|
NotifyOptions,
|
|
174
175
|
NotifyPriority,
|
|
176
|
+
OpenToAllAccessRule,
|
|
177
|
+
OpenToAllDeclaration,
|
|
175
178
|
QualifiedEventName,
|
|
176
179
|
QueryEvent,
|
|
177
180
|
QueryHandlerDef,
|
|
@@ -189,6 +192,7 @@ export type {
|
|
|
189
192
|
WriteHandlerFn,
|
|
190
193
|
WriteResult,
|
|
191
194
|
} from "@cosmicdrift/kumiko-types/handlers";
|
|
195
|
+
export { isOpenToAllGranted } from "@cosmicdrift/kumiko-types/handlers";
|
|
192
196
|
export type {
|
|
193
197
|
DeleteContext,
|
|
194
198
|
EntityHookMap,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// fw#2855 — a write handler's `escapeHatch` gates writes through ctx.db.global(table):
|
|
2
|
+
// WITH it the write succeeds, WITHOUT it the same handler rejects with access_denied.
|
|
3
|
+
// Real HTTP calls + setupTestStack — never createTestDispatcher.
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { defineUnmanagedTable } from "../../db/entity-table-meta";
|
|
8
|
+
import { defineFeature } from "../../engine";
|
|
9
|
+
import { createTestUser, setupTestStack, type TestStack } from "../../stack";
|
|
10
|
+
|
|
11
|
+
const globalStoreTable = defineUnmanagedTable({
|
|
12
|
+
tableName: "store_fw2855_escape_hatch_items",
|
|
13
|
+
tenancy: "global",
|
|
14
|
+
columns: [
|
|
15
|
+
{
|
|
16
|
+
name: "id",
|
|
17
|
+
pgType: "uuid",
|
|
18
|
+
notNull: true,
|
|
19
|
+
primaryKey: true,
|
|
20
|
+
defaultSql: "gen_random_uuid()",
|
|
21
|
+
},
|
|
22
|
+
{ name: "tenant_id", pgType: "uuid", notNull: true },
|
|
23
|
+
{ name: "note", pgType: "text", notNull: true },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const escapeHatchFeature = defineFeature("escape-hatch-probe", (r) => {
|
|
28
|
+
r.storeTable(globalStoreTable, {
|
|
29
|
+
reason: "fw#2855 integration test — cross-tenant store table for db.global()",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
r.writeHandler({
|
|
33
|
+
name: "write-with-hatch",
|
|
34
|
+
schema: z.object({ note: z.string() }),
|
|
35
|
+
access: { roles: ["User"] },
|
|
36
|
+
escapeHatch: { reason: "fw#2855 integration test — declared cross-tenant write" },
|
|
37
|
+
handler: async (event, ctx) => {
|
|
38
|
+
const row = await ctx.db.global(globalStoreTable).insertOne<{ id: string }>({
|
|
39
|
+
tenantId: event.user.tenantId,
|
|
40
|
+
note: event.payload.note,
|
|
41
|
+
});
|
|
42
|
+
return { isSuccess: true as const, data: row };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
r.writeHandler({
|
|
47
|
+
name: "write-without-hatch",
|
|
48
|
+
schema: z.object({ note: z.string() }),
|
|
49
|
+
access: { roles: ["User"] },
|
|
50
|
+
handler: async (event, ctx) => {
|
|
51
|
+
const row = await ctx.db.global(globalStoreTable).insertOne<{ id: string }>({
|
|
52
|
+
tenantId: event.user.tenantId,
|
|
53
|
+
note: event.payload.note,
|
|
54
|
+
});
|
|
55
|
+
return { isSuccess: true as const, data: row };
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let stack: TestStack;
|
|
61
|
+
const user = createTestUser({
|
|
62
|
+
id: 91,
|
|
63
|
+
tenantId: "11111111-0000-4000-8000-000000000091",
|
|
64
|
+
roles: ["User"],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
beforeAll(async () => {
|
|
68
|
+
stack = await setupTestStack({ features: [escapeHatchFeature] });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterAll(async () => {
|
|
72
|
+
await stack.cleanup();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("write handler escapeHatch gates ctx.db.global() writes", () => {
|
|
76
|
+
test("WITH escapeHatch: the write succeeds", async () => {
|
|
77
|
+
const result = await stack.http.writeOk<{ id: string }>(
|
|
78
|
+
"escape-hatch-probe:write:write-with-hatch",
|
|
79
|
+
{ note: "hi" },
|
|
80
|
+
user,
|
|
81
|
+
);
|
|
82
|
+
expect(result.id).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("WITHOUT escapeHatch: the same write shape fails with access_denied", async () => {
|
|
86
|
+
const err = await stack.http.writeErr(
|
|
87
|
+
"escape-hatch-probe:write:write-without-hatch",
|
|
88
|
+
{ note: "hi" },
|
|
89
|
+
user,
|
|
90
|
+
);
|
|
91
|
+
expect(err.code).toBe("access_denied");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -206,6 +206,8 @@ export async function buildHandlerContext(
|
|
|
206
206
|
// but at this point we're the root of the pipeline — cast is safe.
|
|
207
207
|
const dbSource = resolveDbSource(ctx, tx);
|
|
208
208
|
const reqCtx = requestContext.get();
|
|
209
|
+
// db.global()'s write-gate — undefined for query/stream handlers, which can't declare escapeHatch.
|
|
210
|
+
const escapeHatch = registry.getWriteHandler(type)?.escapeHatch;
|
|
209
211
|
const buildTenantScopedDb = (source: DbConnection | DbTx, signal: AbortSignal | undefined) =>
|
|
210
212
|
createTenantDb(
|
|
211
213
|
source,
|
|
@@ -214,6 +216,7 @@ export async function buildHandlerContext(
|
|
|
214
216
|
context.tracer,
|
|
215
217
|
context.meter,
|
|
216
218
|
signal,
|
|
219
|
+
escapeHatch,
|
|
217
220
|
);
|
|
218
221
|
// Propagate the request's AbortSignal so every TenantDb query throws when
|
|
219
222
|
// the client has disconnected — handlers with many sequential queries skip
|
|
@@ -54,6 +54,7 @@ declare const aSecret: Secret<string>;
|
|
|
54
54
|
defineQueryHandler({
|
|
55
55
|
name: "t:query:leak",
|
|
56
56
|
schema,
|
|
57
|
+
access: { openToAll: true },
|
|
57
58
|
handler: async () => ({ apiKey: aSecret }),
|
|
58
59
|
});
|
|
59
60
|
|
|
@@ -64,6 +65,7 @@ function genericResponseHandler<K extends string>(kind: K) {
|
|
|
64
65
|
return defineQueryHandler({
|
|
65
66
|
name: "t:query:generic",
|
|
66
67
|
schema,
|
|
68
|
+
access: { openToAll: true },
|
|
67
69
|
handler: async () => ({ kind, ok: true }) as { kind: K; ok: boolean },
|
|
68
70
|
});
|
|
69
71
|
}
|
|
@@ -81,6 +83,7 @@ declare const leakingUnion: { ok: true } | { apiKey: Secret<string> };
|
|
|
81
83
|
defineQueryHandler({
|
|
82
84
|
name: "t:query:union-leak",
|
|
83
85
|
schema,
|
|
86
|
+
access: { openToAll: true },
|
|
84
87
|
handler: async () => leakingUnion,
|
|
85
88
|
});
|
|
86
89
|
|
|
@@ -88,6 +91,7 @@ declare const cleanUnion: { ok: true } | { id: string };
|
|
|
88
91
|
defineQueryHandler({
|
|
89
92
|
name: "t:query:union-clean",
|
|
90
93
|
schema,
|
|
94
|
+
access: { openToAll: true },
|
|
91
95
|
handler: async () => cleanUnion,
|
|
92
96
|
});
|
|
93
97
|
|
|
@@ -96,6 +100,7 @@ describe("R6 ContainsSecret", () => {
|
|
|
96
100
|
const def = defineQueryHandler({
|
|
97
101
|
name: "t:query:clean",
|
|
98
102
|
schema,
|
|
103
|
+
access: { openToAll: true },
|
|
99
104
|
handler: async () => ({ ok: true, value: createSecret("x").reveal() }),
|
|
100
105
|
});
|
|
101
106
|
expect(def.name).toBe("t:query:clean");
|
|
@@ -34,7 +34,7 @@ function createTasksFeature() {
|
|
|
34
34
|
return defineFeature("tasks", (r) => {
|
|
35
35
|
r.systemScope();
|
|
36
36
|
r.entity("task", taskEntity);
|
|
37
|
-
r.writeHandler(defineEntityCreateHandler("task", taskEntity));
|
|
37
|
+
r.writeHandler(defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }));
|
|
38
38
|
r.screen({
|
|
39
39
|
id: "task-list",
|
|
40
40
|
type: "entityList",
|
|
@@ -105,7 +105,7 @@ describe("generateE2ESpec", () => {
|
|
|
105
105
|
const feature = defineFeature("events", (r) => {
|
|
106
106
|
r.systemScope();
|
|
107
107
|
r.entity("event", entity);
|
|
108
|
-
r.writeHandler(defineEntityCreateHandler("event", entity));
|
|
108
|
+
r.writeHandler(defineEntityCreateHandler("event", entity, { access: { openToAll: true } }));
|
|
109
109
|
r.screen({
|
|
110
110
|
id: "event-edit",
|
|
111
111
|
type: "entityEdit",
|
|
@@ -164,7 +164,9 @@ describe("generateE2ESpec", () => {
|
|
|
164
164
|
const editOnly = defineFeature("edit-only", (r) => {
|
|
165
165
|
r.systemScope();
|
|
166
166
|
r.entity("task", taskEntity);
|
|
167
|
-
r.writeHandler(
|
|
167
|
+
r.writeHandler(
|
|
168
|
+
defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }),
|
|
169
|
+
);
|
|
168
170
|
r.screen({
|
|
169
171
|
id: "edit",
|
|
170
172
|
type: "entityEdit",
|
|
@@ -198,7 +200,9 @@ describe("generateE2ESpec", () => {
|
|
|
198
200
|
const feature = defineFeature("contacts", (r) => {
|
|
199
201
|
r.systemScope();
|
|
200
202
|
r.entity("contact", contactEntity);
|
|
201
|
-
r.writeHandler(
|
|
203
|
+
r.writeHandler(
|
|
204
|
+
defineEntityCreateHandler("contact", contactEntity, { access: { openToAll: true } }),
|
|
205
|
+
);
|
|
202
206
|
r.screen({
|
|
203
207
|
id: "list",
|
|
204
208
|
type: "entityList",
|
|
@@ -220,7 +224,9 @@ describe("generateE2ESpec", () => {
|
|
|
220
224
|
const mixed = defineFeature("mixed", (r) => {
|
|
221
225
|
r.systemScope();
|
|
222
226
|
r.entity("task", taskEntity);
|
|
223
|
-
r.writeHandler(
|
|
227
|
+
r.writeHandler(
|
|
228
|
+
defineEntityCreateHandler("task", taskEntity, { access: { openToAll: true } }),
|
|
229
|
+
);
|
|
224
230
|
r.screen({ id: "list", type: "entityList", entity: "task", columns: ["title"] });
|
|
225
231
|
r.screen({
|
|
226
232
|
id: "edit",
|
package/src/ui-types/index.ts
CHANGED
|
@@ -52,7 +52,12 @@ export type {
|
|
|
52
52
|
SelectFieldDef,
|
|
53
53
|
TextFieldDef,
|
|
54
54
|
} from "../engine/types/fields";
|
|
55
|
-
export type {
|
|
55
|
+
export type {
|
|
56
|
+
AccessRule,
|
|
57
|
+
OpenToAllAccessRule,
|
|
58
|
+
OpenToAllDeclaration,
|
|
59
|
+
} from "../engine/types/handlers";
|
|
60
|
+
export { isOpenToAllGranted } from "../engine/types/handlers";
|
|
56
61
|
export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav";
|
|
57
62
|
export type {
|
|
58
63
|
ActionFormRedirect,
|