@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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/AGENTS.md +338 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +446 -0
- package/agents/examples/anonymous-access.md +288 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +319 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +315 -0
- package/agents/examples/custom-handlers.md +444 -0
- package/agents/examples/dashboard-stats.md +229 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +414 -0
- package/agents/examples/error-contract.md +535 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +274 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +215 -0
- package/agents/examples/ledger-invoicing.md +236 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +346 -0
- package/agents/examples/ownership.md +404 -0
- package/agents/examples/pdf-audit-export.md +306 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +250 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +418 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +75 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +7 -0
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +49 -46
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +46 -17
- package/dist/api/server.d.ts +4 -3
- package/dist/api/server.js +39 -10
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +15 -3
- package/dist/bun-db/query.js +76 -24
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +4 -3
- package/dist/db/event-store-executor-context.d.ts +6 -6
- package/dist/db/event-store-executor-context.js +25 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +84 -63
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +2 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +64 -38
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +4 -0
- package/dist/db/tenant-db.js +50 -5
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +87 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -8
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +14 -2
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +1 -1
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/_read-source.js +5 -2
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +24 -1
- package/dist/files/create-file-ref.js +67 -25
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +12 -0
- package/dist/files/file-routes.js +10 -6
- package/dist/files/index.d.ts +3 -3
- package/dist/files/index.js +2 -2
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +46 -21
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +5 -1
- package/dist/jobs/job-runner.js +124 -37
- package/dist/observability/escape-hatch-report.d.ts +2 -1
- package/dist/observability/escape-hatch-report.js +48 -7
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +136 -48
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +29 -9
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/schema-cli.js +12 -1
- package/dist/search/meilisearch-adapter.js +36 -9
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +7 -5
- package/src/changes.json +512 -8
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/access-control. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# access-control
|
|
4
|
+
|
|
5
|
+
Default-deny access rules, openToAll vs. role-based handlers, and auto-indices for foreign keys.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# Access Control
|
|
10
|
+
|
|
11
|
+
Default-deny access rules and FK indices via relations.
|
|
12
|
+
|
|
13
|
+
## What it shows
|
|
14
|
+
|
|
15
|
+
- Handler `access` roles as the default-deny gate
|
|
16
|
+
- Relation-declared FK indices for join-friendly schemas
|
|
17
|
+
|
|
18
|
+
## Source
|
|
19
|
+
|
|
20
|
+
Feature entry point: `src/feature.ts`.
|
|
21
|
+
|
|
22
|
+
## Tests
|
|
23
|
+
|
|
24
|
+
Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd samples/recipes/access-control
|
|
28
|
+
bun test
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Source files
|
|
32
|
+
|
|
33
|
+
### src/__tests__/feature.integration.test.ts
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
37
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
38
|
+
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
39
|
+
import {
|
|
40
|
+
setupTestStack,
|
|
41
|
+
type TestStack,
|
|
42
|
+
TestUsers,
|
|
43
|
+
unsafeCreateEntityTable,
|
|
44
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
45
|
+
import { z } from "zod";
|
|
46
|
+
import {
|
|
47
|
+
accessControlFeature,
|
|
48
|
+
projectEntity,
|
|
49
|
+
projectTable,
|
|
50
|
+
taskEntity,
|
|
51
|
+
taskTable,
|
|
52
|
+
} from "../feature";
|
|
53
|
+
|
|
54
|
+
let stack: TestStack;
|
|
55
|
+
const admin = TestUsers.admin; // { id: 1, tenantId: 1, roles: ["Admin"] }
|
|
56
|
+
const user = TestUsers.user; // { id: 2, tenantId: 1, roles: ["User"] }
|
|
57
|
+
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
stack = await setupTestStack({ features: [accessControlFeature] });
|
|
60
|
+
await unsafeCreateEntityTable(stack.db, projectEntity);
|
|
61
|
+
await unsafeCreateEntityTable(stack.db, taskEntity);
|
|
62
|
+
await createEventsTable(stack.db);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
await stack?.cleanup();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("default-deny: refuses handlers without access", () => {
|
|
70
|
+
test("throws at registration when a handler lacks an access rule", () => {
|
|
71
|
+
// `access` is required since fw#2855 — the registrar throws before validateBoot ever runs.
|
|
72
|
+
expect(() =>
|
|
73
|
+
defineFeature("broken-feature", (r) => {
|
|
74
|
+
// @ts-expect-error — No { access: ... }: registration must reject this.
|
|
75
|
+
r.writeHandler("forgotAccess", z.object({ name: z.string() }), async () => ({
|
|
76
|
+
isSuccess: true as const,
|
|
77
|
+
data: {},
|
|
78
|
+
}));
|
|
79
|
+
}),
|
|
80
|
+
).toThrow(/requires schema \+ handler \+ options\.access/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("role-based vs openToAll", () => {
|
|
85
|
+
test("Admin-only handler rejects a User caller with access_denied", async () => {
|
|
86
|
+
const raw = await stack.http.write(
|
|
87
|
+
"access-control:write:project:create",
|
|
88
|
+
{ name: "Rejected", ownerId: "11111111-0000-4000-8000-000000000001" },
|
|
89
|
+
user,
|
|
90
|
+
);
|
|
91
|
+
const body = (await raw.json()) as { isSuccess: boolean; error?: { code: string } };
|
|
92
|
+
expect(body.isSuccess).toBe(false);
|
|
93
|
+
expect(body.error?.code).toBe("access_denied");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("Admin-only handler accepts an Admin caller", async () => {
|
|
97
|
+
await stack.http.writeOk(
|
|
98
|
+
"access-control:write:project:create",
|
|
99
|
+
{ name: "Alpha", ownerId: "11111111-0000-4000-8000-000000000001" },
|
|
100
|
+
admin,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("openToAll handler accepts any authenticated user", async () => {
|
|
105
|
+
const proj = await stack.http.writeOk<{ data: { id: string } }>(
|
|
106
|
+
"access-control:write:project:create",
|
|
107
|
+
{ name: "Shared", ownerId: "11111111-0000-4000-8000-000000000001" },
|
|
108
|
+
admin,
|
|
109
|
+
);
|
|
110
|
+
await stack.http.writeOk(
|
|
111
|
+
"access-control:write:task:create",
|
|
112
|
+
{ title: "User's task", projectId: proj.data.id },
|
|
113
|
+
user,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("auto-indices from relations", () => {
|
|
119
|
+
test("task table has an index on project_id because of the belongsTo relation", () => {
|
|
120
|
+
const names = taskTable.indexes.map((i) => i.name);
|
|
121
|
+
|
|
122
|
+
expect(names).toContain("read_ac_tasks_tenant_id_idx");
|
|
123
|
+
expect(names).toContain("read_ac_tasks_project_id_idx");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("project table has only the tenant_id index (no belongsTo relations)", () => {
|
|
127
|
+
expect(projectTable.indexes.map((i) => i.name)).toEqual(["read_ac_projects_tenant_id_idx"]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("optimistic locking: version required in update schema", () => {
|
|
132
|
+
test("update without version fails schema validation before reaching the crud executor", async () => {
|
|
133
|
+
const proj = await stack.http.writeOk<{ data: { id: string } }>(
|
|
134
|
+
"access-control:write:project:create",
|
|
135
|
+
{ name: "VersionedProject", ownerId: "11111111-0000-4000-8000-000000000001" },
|
|
136
|
+
admin,
|
|
137
|
+
);
|
|
138
|
+
const created = await stack.http.writeOk<{ data: { id: string } }>(
|
|
139
|
+
"access-control:write:task:create",
|
|
140
|
+
{ title: "Needs version", projectId: proj.data.id },
|
|
141
|
+
user,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const raw = await stack.http.write(
|
|
145
|
+
"access-control:write:task:update",
|
|
146
|
+
// Missing `version` — schema rejects before any DB call.
|
|
147
|
+
{ id: created.data.id, changes: { title: "Updated" } },
|
|
148
|
+
user,
|
|
149
|
+
);
|
|
150
|
+
const body = (await raw.json()) as { isSuccess: boolean; error?: { code: string } };
|
|
151
|
+
expect(body.isSuccess).toBe(false);
|
|
152
|
+
expect(body.error?.code).toBe("validation_error");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("update with correct version succeeds", async () => {
|
|
156
|
+
const proj = await stack.http.writeOk<{ data: { id: string } }>(
|
|
157
|
+
"access-control:write:project:create",
|
|
158
|
+
{ name: "VersionedOk", ownerId: "11111111-0000-4000-8000-000000000001" },
|
|
159
|
+
admin,
|
|
160
|
+
);
|
|
161
|
+
const created = await stack.http.writeOk<{ data: { id: string } }>(
|
|
162
|
+
"access-control:write:task:create",
|
|
163
|
+
{ title: "Task v1", projectId: proj.data.id },
|
|
164
|
+
user,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
await stack.http.writeOk(
|
|
168
|
+
"access-control:write:task:update",
|
|
169
|
+
{ id: created.data.id, version: 1, changes: { title: "Task v2" } },
|
|
170
|
+
user,
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Ensures this sample is wired through the full stack — if any of its
|
|
176
|
+
// handlers regress to missing access or broken schemas, boot or this list
|
|
177
|
+
// query will fail loudly.
|
|
178
|
+
describe("end-to-end", () => {
|
|
179
|
+
test("list returns seeded tasks across users", async () => {
|
|
180
|
+
const proj = await stack.http.writeOk<{ data: { id: string } }>(
|
|
181
|
+
"access-control:write:project:create",
|
|
182
|
+
{ name: "ListTarget", ownerId: "11111111-0000-4000-8000-000000000001" },
|
|
183
|
+
admin,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await stack.http.writeOk(
|
|
187
|
+
"access-control:write:task:create",
|
|
188
|
+
{ title: "t1", projectId: proj.data.id },
|
|
189
|
+
user,
|
|
190
|
+
);
|
|
191
|
+
await stack.http.writeOk(
|
|
192
|
+
"access-control:write:task:create",
|
|
193
|
+
{ title: "t2", projectId: proj.data.id },
|
|
194
|
+
admin,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const list = await stack.http.queryOk<{ rows: Record<string, unknown>[] }>(
|
|
198
|
+
"access-control:query:task:list",
|
|
199
|
+
{},
|
|
200
|
+
user,
|
|
201
|
+
);
|
|
202
|
+
expect(list.rows.length).toBeGreaterThanOrEqual(2);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### src/feature.ts
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// Access Control + FK-Indices Sample
|
|
211
|
+
//
|
|
212
|
+
// Demonstrates the framework's security + performance defaults:
|
|
213
|
+
//
|
|
214
|
+
// 1. Default-deny: every handler must declare an access rule. Boot fails
|
|
215
|
+
// loudly when one is missing. Two shapes:
|
|
216
|
+
// { roles: ["Admin", ...] } — role allowlist
|
|
217
|
+
// { openToAll: { reason: "..." } } — any authenticated user, with a required reason
|
|
218
|
+
//
|
|
219
|
+
// 2. Foreign-key relations declared via r.relation() become indexed columns
|
|
220
|
+
// automatically — no manual CREATE INDEX. Inspect the Drizzle table
|
|
221
|
+
// config in tests to verify.
|
|
222
|
+
|
|
223
|
+
import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
224
|
+
import {
|
|
225
|
+
createEntity,
|
|
226
|
+
createTextField,
|
|
227
|
+
defineEntityCreateHandler,
|
|
228
|
+
defineEntityListHandler,
|
|
229
|
+
defineFeature,
|
|
230
|
+
defineRoles,
|
|
231
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
232
|
+
import { z } from "zod";
|
|
233
|
+
|
|
234
|
+
// Typed role registry — `defineRoles` returns an object keyed by role name
|
|
235
|
+
// so access rules can reference `AccessControlRoles.Admin` instead of the
|
|
236
|
+
// magic string "Admin". Typos become compile errors, renames refactor.
|
|
237
|
+
export const AccessControlRoles = defineRoles(["Admin"] as const);
|
|
238
|
+
|
|
239
|
+
export const projectEntity = createEntity({
|
|
240
|
+
table: "read_ac_projects",
|
|
241
|
+
fields: {
|
|
242
|
+
name: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
243
|
+
ownerId: createTextField({ personal: "ref", required: true }),
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
export const taskEntity = createEntity({
|
|
248
|
+
table: "read_ac_tasks",
|
|
249
|
+
fields: {
|
|
250
|
+
title: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
251
|
+
assigneeId: createTextField({ personal: "ref" }),
|
|
252
|
+
projectId: createTextField({ personal: false, reason: "is_system_identifier", required: true }),
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// The "owner" relation points at a hypothetical external `user` entity — in
|
|
257
|
+
// a real app this would be @cosmicdrift/kumiko-bundled-features/user. For the sample we
|
|
258
|
+
// keep it internal: projectRelations only holds FK metadata (no cross-entity
|
|
259
|
+
// resolution) so the sample stays self-contained.
|
|
260
|
+
export const projectRelations = {} as const;
|
|
261
|
+
|
|
262
|
+
export const taskRelations = {
|
|
263
|
+
project: { type: "belongsTo", target: "project", foreignKey: "projectId" },
|
|
264
|
+
} as const;
|
|
265
|
+
|
|
266
|
+
// Relations flow into buildEntityTable — every belongsTo FK gets its own
|
|
267
|
+
// index on this table.
|
|
268
|
+
export const projectTable = buildEntityTable("project", projectEntity, {
|
|
269
|
+
relations: projectRelations,
|
|
270
|
+
});
|
|
271
|
+
export const taskTable = buildEntityTable("task", taskEntity, {
|
|
272
|
+
relations: taskRelations,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
export const accessControlFeature = defineFeature("access-control", (r) => {
|
|
276
|
+
r.entity("project", projectEntity);
|
|
277
|
+
r.entity("task", taskEntity);
|
|
278
|
+
r.relation("task", "project", taskRelations.project);
|
|
279
|
+
|
|
280
|
+
// Admin-only: standard create — no business logic, helper handles schema +
|
|
281
|
+
// executor + handler-body.
|
|
282
|
+
r.writeHandler(
|
|
283
|
+
defineEntityCreateHandler("project", projectEntity, {
|
|
284
|
+
access: { roles: [AccessControlRoles.Admin] },
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Custom create: assigneeId defaults to the caller when omitted, so we
|
|
289
|
+
// hand-write this handler instead of using the helper. The framework helpers
|
|
290
|
+
// are deliberately scope-limited — opt out the moment you need defaults.
|
|
291
|
+
const taskExecutor = createEventStoreExecutor(taskTable, taskEntity, { entityName: "task" });
|
|
292
|
+
r.writeHandler(
|
|
293
|
+
"task:create",
|
|
294
|
+
z.object({
|
|
295
|
+
title: z.string().min(1),
|
|
296
|
+
projectId: z.uuid(),
|
|
297
|
+
assigneeId: z.uuid().optional(),
|
|
298
|
+
}),
|
|
299
|
+
async (event, ctx) =>
|
|
300
|
+
taskExecutor.create(
|
|
301
|
+
{
|
|
302
|
+
...event.payload,
|
|
303
|
+
assigneeId: event.payload.assigneeId ?? event.user.id,
|
|
304
|
+
},
|
|
305
|
+
event.user,
|
|
306
|
+
ctx.db,
|
|
307
|
+
),
|
|
308
|
+
{
|
|
309
|
+
access: {
|
|
310
|
+
openToAll: {
|
|
311
|
+
reason:
|
|
312
|
+
"any signed-in tenant user may create a task; assigneeId defaults to the caller when omitted",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Update requires version — optimistic locking is the default and this
|
|
319
|
+
// narrowed schema (only title + assigneeId mutable) is stricter than the
|
|
320
|
+
// helper's generic update schema, so we keep it hand-written.
|
|
321
|
+
r.writeHandler(
|
|
322
|
+
"task:update",
|
|
323
|
+
z.object({
|
|
324
|
+
id: z.uuid(),
|
|
325
|
+
version: z.number(),
|
|
326
|
+
changes: z.object({
|
|
327
|
+
title: z.string().min(1).optional(),
|
|
328
|
+
assigneeId: z.uuid().optional(),
|
|
329
|
+
}),
|
|
330
|
+
}),
|
|
331
|
+
async (event, ctx) => taskExecutor.update(event.payload, event.user, ctx.db),
|
|
332
|
+
{
|
|
333
|
+
access: {
|
|
334
|
+
openToAll: {
|
|
335
|
+
reason:
|
|
336
|
+
"any signed-in tenant user may update a task; optimistic locking (version) " +
|
|
337
|
+
"guards against concurrent overwrites, not caller identity",
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Self-service list — no custom logic.
|
|
344
|
+
r.queryHandler(
|
|
345
|
+
defineEntityListHandler("task", taskEntity, {
|
|
346
|
+
access: {
|
|
347
|
+
openToAll: {
|
|
348
|
+
reason:
|
|
349
|
+
"any signed-in tenant user may list tasks; there is no per-user filter in this recipe",
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
```
|