@cosmicdrift/kumiko-framework 0.257.0 → 0.258.1
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/__tests__/anonymous-access.integration.test.ts +3 -3
- package/src/__tests__/derived-fields.integration.test.ts +1 -1
- package/src/__tests__/entity-list-limits.integration.test.ts +1 -1
- package/src/__tests__/error-contract.integration.test.ts +1 -1
- package/src/__tests__/ownership-where-write-path.integration.test.ts +1 -1
- package/src/__tests__/ownership.integration.test.ts +7 -3
- package/src/__tests__/schema-cli-rebuild.integration.test.ts +2 -2
- package/src/__tests__/transition-guard.integration.test.ts +3 -3
- package/src/api/__tests__/api.test.ts +21 -3
- package/src/api/__tests__/batch.integration.test.ts +3 -3
- package/src/api/__tests__/body-limit.test.ts +7 -1
- package/src/api/__tests__/dispatcher-live.integration.test.ts +1 -1
- package/src/api/__tests__/http-route-rate-limit.integration.test.ts +7 -1
- package/src/api/__tests__/metrics-endpoint.test.ts +7 -1
- package/src/api/__tests__/nested-write.integration.test.ts +3 -3
- package/src/api/__tests__/server-boot-guards.test.ts +11 -4
- package/src/bun-db/__tests__/write-brand.test.ts +4 -1
- package/src/db/__tests__/apply-entity-event-tenant.integration.test.ts +1 -1
- package/src/db/__tests__/assert-exists-in.integration.test.ts +1 -1
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +2 -2
- package/src/db/__tests__/collect-table-metas.test.ts +9 -5
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/config-seed.integration.test.ts +3 -3
- package/src/db/__tests__/eagerload.integration.test.ts +7 -7
- package/src/db/__tests__/entity-table-from-registry.test.ts +8 -2
- package/src/db/__tests__/entity-table-meta-source.test.ts +7 -3
- package/src/db/__tests__/entity-table-types.test.ts +4 -4
- package/src/db/__tests__/event-store-executor-list.integration.test.ts +9 -2
- package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +4 -4
- package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +3 -3
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +4 -4
- package/src/db/__tests__/event-store-executor.integration.test.ts +11 -6
- package/src/db/__tests__/event-store-system-stream.integration.test.ts +2 -2
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +6 -4
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/multi-row-insert.integration.test.ts +3 -3
- package/src/db/__tests__/parent-ref-clause.test.ts +213 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +30 -14
- package/src/db/__tests__/source-shadow-create.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-indexes.test.ts +14 -12
- package/src/db/__tests__/table-builder-required.test.ts +2 -2
- package/src/db/__tests__/tenant-db-where-merge.integration.test.ts +1 -1
- package/src/db/__tests__/tenant-db-where-merge.test.ts +5 -2
- package/src/db/__tests__/tenant-db.integration.test.ts +2 -2
- package/src/db/__tests__/unique-violation-mapping.integration.test.ts +2 -2
- package/src/db/event-store-executor-read.ts +97 -19
- package/src/db/parent-ref-clause.ts +182 -0
- package/src/db/table-builder.ts +7 -0
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +50 -8
- package/src/engine/__tests__/boot-validator-detail-for.test.ts +28 -4
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +20 -4
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +9 -3
- package/src/engine/__tests__/boot-validator.test.ts +716 -91
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +2 -2
- package/src/engine/__tests__/engine.test.ts +128 -37
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +4 -4
- package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +1 -1
- package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +1 -1
- package/src/engine/__tests__/extend-entity-projection.test.ts +1 -1
- package/src/engine/__tests__/extends-registrar.test.ts +13 -3
- package/src/engine/__tests__/factories-long-text.test.ts +10 -6
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +1 -1
- package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
- package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +1 -1
- package/src/engine/__tests__/projection-detail-actions.test.ts +6 -1
- package/src/engine/__tests__/projection-detail-singleton.test.ts +6 -1
- package/src/engine/__tests__/projection-detail-tabs.test.ts +6 -1
- package/src/engine/__tests__/projection-list-filter.integration.test.ts +7 -2
- package/src/engine/__tests__/projection.test.ts +1 -1
- package/src/engine/__tests__/registry.test.ts +13 -3
- package/src/engine/__tests__/schema-builder.test.ts +39 -11
- package/src/engine/__tests__/screen.test.ts +21 -15
- package/src/engine/__tests__/search-payload-extension.test.ts +15 -3
- package/src/engine/__tests__/store-table.test.ts +6 -3
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +12 -4
- package/src/engine/__tests__/validation-hooks.test.ts +7 -1
- package/src/engine/boot-validator/__tests__/ownership.test.ts +2 -2
- package/src/engine/boot-validator/__tests__/parent-ref.test.ts +149 -0
- package/src/engine/boot-validator/action-wiring.ts +7 -7
- package/src/engine/boot-validator/index.ts +2 -0
- package/src/engine/boot-validator/parent-ref.ts +96 -0
- package/src/engine/boot-validator/projection-list-screens.ts +36 -23
- package/src/engine/boot-validator/query-output-columns.ts +2 -1
- package/src/engine/boot-validator/query-refs.ts +2 -1
- package/src/engine/boot-validator/screens.ts +322 -131
- package/src/engine/build-config-feature-schema.ts +2 -2
- package/src/engine/define-handler.ts +6 -0
- package/src/engine/entity-handlers.ts +6 -1
- package/src/engine/factories.ts +2 -0
- package/src/engine/feature-entity-handlers.ts +5 -0
- package/src/engine/index.ts +6 -0
- package/src/engine/ownership.ts +15 -0
- package/src/engine/pattern-library/mixed-schemas.ts +1 -0
- package/src/engine/screen-helpers.ts +25 -0
- package/src/engine/types/index.ts +5 -0
- package/src/es-ops/__tests__/runner.integration.test.ts +1 -1
- package/src/event-store/__tests__/upcaster.integration.test.ts +1 -1
- package/src/files/__tests__/file-field-pipeline.integration.test.ts +1 -1
- package/src/files/__tests__/files.integration.test.ts +1 -1
- package/src/files/file-ref-entity.ts +9 -9
- package/src/i18n/__tests__/i18n.test.ts +7 -1
- package/src/i18n/required-surface-keys.ts +48 -0
- package/src/migrations/__tests__/kumiko-drift.integration.test.ts +3 -1
- package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +2 -2
- package/src/pipeline/__tests__/archive-stream.integration.test.ts +1 -1
- package/src/pipeline/__tests__/cascade-handler.integration.test.ts +23 -8
- package/src/pipeline/__tests__/cascade-handler.test.ts +18 -3
- package/src/pipeline/__tests__/causation-chain.integration.test.ts +1 -1
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +5 -3
- package/src/pipeline/__tests__/dispatcher.test.ts +42 -6
- package/src/pipeline/__tests__/domain-events-projections.integration.test.ts +2 -2
- package/src/pipeline/__tests__/fetch-for-writing.integration.test.ts +1 -1
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +42 -6
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +2 -2
- package/src/pipeline/__tests__/msp-multi-hop.integration.test.ts +1 -1
- package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +6 -2
- package/src/pipeline/__tests__/multi-stream-projection.integration.test.ts +6 -2
- package/src/pipeline/__tests__/perf-rebuild.integration.test.ts +1 -1
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +3 -3
- package/src/pipeline/__tests__/query-projection.integration.test.ts +1 -1
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +2 -2
- package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +13 -2
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +1 -1
- package/src/search/__tests__/reindex-entity.integration.test.ts +6 -1
- package/src/search/__tests__/search-pii-derived-index.integration.test.ts +7 -1
- package/src/stack/__tests__/ownership-boot-guard.integration.test.ts +1 -1
- package/src/testing/__tests__/e2e-generator.test.ts +15 -5
- package/src/testing/e2e-generator.ts +2 -0
- package/src/testing/shared-entities.ts +4 -2
- package/src/ui-types/index.ts +4 -0
|
@@ -23,7 +23,7 @@ import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
|
23
23
|
const customerEntity = createEntity({
|
|
24
24
|
table: "read_ref_search_customers",
|
|
25
25
|
fields: {
|
|
26
|
-
name: createTextField({ required: true }),
|
|
26
|
+
name: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
29
|
const customerTable = buildEntityTable("refSearchCustomer", customerEntity);
|
|
@@ -31,7 +31,7 @@ const customerTable = buildEntityTable("refSearchCustomer", customerEntity);
|
|
|
31
31
|
const orderEntity = createEntity({
|
|
32
32
|
table: "read_ref_search_orders",
|
|
33
33
|
fields: {
|
|
34
|
-
note: createTextField({ searchable: true }),
|
|
34
|
+
note: createTextField({ searchable: true, personal: false, reason: "test_fixture" }),
|
|
35
35
|
customerId: {
|
|
36
36
|
type: "reference",
|
|
37
37
|
entity: "refSearchCustomer",
|
|
@@ -48,7 +48,7 @@ const orderTable = buildEntityTable("refSearchOrder", orderEntity);
|
|
|
48
48
|
const restrictedOrderEntity = createEntity({
|
|
49
49
|
table: "read_ref_search_restricted_orders",
|
|
50
50
|
fields: {
|
|
51
|
-
note: createTextField({ searchable: true }),
|
|
51
|
+
note: createTextField({ searchable: true, personal: false, reason: "test_fixture" }),
|
|
52
52
|
customerId: {
|
|
53
53
|
type: "reference",
|
|
54
54
|
entity: "refSearchCustomer",
|
|
@@ -66,7 +66,7 @@ const restrictedOrderTable = buildEntityTable("refSearchRestrictedOrder", restri
|
|
|
66
66
|
// (LIST_ROW_META_REFERENCES) is what's under test, not tenant's full schema.
|
|
67
67
|
const tenantMetaEntity = createEntity({
|
|
68
68
|
table: "read_tenants",
|
|
69
|
-
fields: { name: createTextField({ required: true }) },
|
|
69
|
+
fields: { name: createTextField({ required: true, personal: false, reason: "test_fixture" }) },
|
|
70
70
|
});
|
|
71
71
|
const tenantMetaTable = buildEntityTable("tenant", tenantMetaEntity);
|
|
72
72
|
|
|
@@ -19,14 +19,14 @@ import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
|
19
19
|
|
|
20
20
|
const customerEntity = createEntity({
|
|
21
21
|
table: "read_ref_sort_customers",
|
|
22
|
-
fields: { name: createTextField({ required: true }) },
|
|
22
|
+
fields: { name: createTextField({ required: true, personal: false, reason: "test_fixture" }) },
|
|
23
23
|
});
|
|
24
24
|
const customerTable = buildEntityTable("refSortCustomer", customerEntity);
|
|
25
25
|
|
|
26
26
|
const orderEntity = createEntity({
|
|
27
27
|
table: "read_ref_sort_orders",
|
|
28
28
|
fields: {
|
|
29
|
-
note: createTextField(),
|
|
29
|
+
note: createTextField({ personal: false, reason: "test_fixture" }),
|
|
30
30
|
customerId: {
|
|
31
31
|
type: "reference",
|
|
32
32
|
entity: "refSortCustomer",
|
|
@@ -43,7 +43,7 @@ const orderTable = buildEntityTable("refSortOrder", orderEntity);
|
|
|
43
43
|
const restrictedOrderEntity = createEntity({
|
|
44
44
|
table: "read_ref_sort_restricted_orders",
|
|
45
45
|
fields: {
|
|
46
|
-
note: createTextField(),
|
|
46
|
+
note: createTextField({ personal: false, reason: "test_fixture" }),
|
|
47
47
|
customerId: {
|
|
48
48
|
type: "reference",
|
|
49
49
|
entity: "refSortCustomer",
|
|
@@ -44,7 +44,7 @@ afterAll(async () => {
|
|
|
44
44
|
const restrictedEntity = createEntity({
|
|
45
45
|
table: "read_es_write_restricted",
|
|
46
46
|
fields: {
|
|
47
|
-
email: createTextField({ required: true }),
|
|
47
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
48
48
|
},
|
|
49
49
|
softDelete: true,
|
|
50
50
|
access: { write: { Admin: "all" } },
|
|
@@ -189,7 +189,7 @@ describe("event-store-executor write-verbs — entity-level ownership_denied", (
|
|
|
189
189
|
const hardDeleteEntity = createEntity({
|
|
190
190
|
table: "read_es_write_hard",
|
|
191
191
|
fields: {
|
|
192
|
-
email: createTextField({ required: true }),
|
|
192
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
193
193
|
},
|
|
194
194
|
});
|
|
195
195
|
const hardDeleteTable = buildEntityTable("esWriteHard", hardDeleteEntity);
|
|
@@ -340,7 +340,7 @@ describe("event-store-executor write-verbs — field-level ownership_denied", ()
|
|
|
340
340
|
const versionEntity = createEntity({
|
|
341
341
|
table: "read_es_write_version",
|
|
342
342
|
fields: {
|
|
343
|
-
email: createTextField({ required: true }),
|
|
343
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
344
344
|
},
|
|
345
345
|
});
|
|
346
346
|
const versionTable = buildEntityTable("esWriteVersion", versionEntity);
|
|
@@ -394,7 +394,7 @@ describe("event-store-executor write-verbs — version_conflict edge cases", ()
|
|
|
394
394
|
const raceEntity = createEntity({
|
|
395
395
|
table: "read_es_write_race",
|
|
396
396
|
fields: {
|
|
397
|
-
email: createTextField({ required: true }),
|
|
397
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
398
398
|
},
|
|
399
399
|
softDelete: true,
|
|
400
400
|
});
|
|
@@ -21,8 +21,13 @@ import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
|
21
21
|
const entity = createEntity({
|
|
22
22
|
table: "read_es_exec_users",
|
|
23
23
|
fields: {
|
|
24
|
-
email: createTextField({
|
|
25
|
-
|
|
24
|
+
email: createTextField({
|
|
25
|
+
required: true,
|
|
26
|
+
searchable: true,
|
|
27
|
+
personal: false,
|
|
28
|
+
reason: "test_fixture",
|
|
29
|
+
}),
|
|
30
|
+
firstName: createTextField({ personal: false, reason: "test_fixture" }),
|
|
26
31
|
isEnabled: createBooleanField({ default: true }),
|
|
27
32
|
},
|
|
28
33
|
softDelete: true,
|
|
@@ -133,7 +138,7 @@ const SENSITIVE_TEST_KEY = Buffer.from("s3nS!t1vE.kP9xQ2@wN!vL$hR5yT8eU0").toStr
|
|
|
133
138
|
const sensitiveEntity = createEntity({
|
|
134
139
|
table: "read_es_exec_sensitive",
|
|
135
140
|
fields: {
|
|
136
|
-
email: createTextField({ required: true }),
|
|
141
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
137
142
|
passwordHash: createTextField({ sensitive: true, encrypted: true }),
|
|
138
143
|
apiToken: createTextField({ sensitive: true, encrypted: true }),
|
|
139
144
|
},
|
|
@@ -337,7 +342,7 @@ const ENCRYPTION_TEST_KEY = Buffer.from("a]bJm#kP9xQ2@wN!vL$hR5yT8eU0iO3f").toSt
|
|
|
337
342
|
const encryptedEntity = createEntity({
|
|
338
343
|
table: "read_es_exec_encrypted",
|
|
339
344
|
fields: {
|
|
340
|
-
email: createTextField({ required: true }),
|
|
345
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
341
346
|
secretNote: createTextField({ encrypted: true }),
|
|
342
347
|
},
|
|
343
348
|
});
|
|
@@ -346,7 +351,7 @@ const encryptedTable = buildEntityTable("esExecEncrypted", encryptedEntity);
|
|
|
346
351
|
const encryptedSoftDeleteEntity = createEntity({
|
|
347
352
|
table: "read_es_exec_enc_soft",
|
|
348
353
|
fields: {
|
|
349
|
-
email: createTextField({ required: true }),
|
|
354
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
350
355
|
secretNote: createTextField({ encrypted: true }),
|
|
351
356
|
},
|
|
352
357
|
softDelete: true,
|
|
@@ -699,7 +704,7 @@ const piiEntity = createEntity({
|
|
|
699
704
|
find: "none",
|
|
700
705
|
}),
|
|
701
706
|
authorId: createTextField(),
|
|
702
|
-
plain: createTextField(),
|
|
707
|
+
plain: createTextField({ personal: false, reason: "test_fixture" }),
|
|
703
708
|
},
|
|
704
709
|
});
|
|
705
710
|
const piiTable = buildEntityTable("esExecPii", piiEntity);
|
|
@@ -22,11 +22,11 @@ import { createTenantDb } from "../tenant-db";
|
|
|
22
22
|
const systemEntity = createEntity({
|
|
23
23
|
table: "sstream_sys",
|
|
24
24
|
systemStream: true,
|
|
25
|
-
fields: { name: createTextField({ required: true }) },
|
|
25
|
+
fields: { name: createTextField({ required: true, personal: false, reason: "test_fixture" }) },
|
|
26
26
|
});
|
|
27
27
|
const tenantOwnedEntity = createEntity({
|
|
28
28
|
table: "sstream_tn",
|
|
29
|
-
fields: { name: createTextField({ required: true }) },
|
|
29
|
+
fields: { name: createTextField({ required: true, personal: false, reason: "test_fixture" }) },
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
const systemTable = buildEntityTable("sstreamSys", systemEntity);
|
|
@@ -31,8 +31,8 @@ import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
|
31
31
|
const userEntity = createEntity({
|
|
32
32
|
table: "read_implicit_users",
|
|
33
33
|
fields: {
|
|
34
|
-
email: createTextField({ required: true }),
|
|
35
|
-
firstName: createTextField(),
|
|
34
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
35
|
+
firstName: createTextField({ personal: false, reason: "test_fixture" }),
|
|
36
36
|
isEnabled: createBooleanField({ default: true }),
|
|
37
37
|
},
|
|
38
38
|
softDelete: true,
|
|
@@ -204,7 +204,9 @@ describe("implicit-projection / Live==Rebuild equivalence", () => {
|
|
|
204
204
|
test("ohne softDelete → keine restore-apply-key registriert", () => {
|
|
205
205
|
const hardDeleteEntity = createEntity({
|
|
206
206
|
table: "read_implicit_hard",
|
|
207
|
-
fields: {
|
|
207
|
+
fields: {
|
|
208
|
+
name: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
209
|
+
},
|
|
208
210
|
});
|
|
209
211
|
const hardFeature = defineFeature("implicithard", (r) => {
|
|
210
212
|
r.entity("widget", hardDeleteEntity);
|
|
@@ -249,7 +251,7 @@ const SENSITIVE_BIDX_KEY = decodeBlindIndexKey(SENSITIVE_BIDX_KEY_B64);
|
|
|
249
251
|
const sensitiveEntity = createEntity({
|
|
250
252
|
table: sensitiveTable,
|
|
251
253
|
fields: {
|
|
252
|
-
email: createTextField({ required: true }),
|
|
254
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
253
255
|
apiKey: createTextField({ personal: "self", find: "exact" }),
|
|
254
256
|
},
|
|
255
257
|
});
|
|
@@ -8,7 +8,7 @@ import { flattenLocatedTimestamp, rehydrateLocatedTimestamp } from "../located-t
|
|
|
8
8
|
|
|
9
9
|
const orderEntity: EntityDefinition = createEntity({
|
|
10
10
|
fields: {
|
|
11
|
-
clientName: createTextField(),
|
|
11
|
+
clientName: createTextField({ personal: false, reason: "test_fixture" }),
|
|
12
12
|
pickup: createLocatedTimestampField(),
|
|
13
13
|
delivery: createLocatedTimestampField(),
|
|
14
14
|
},
|
|
@@ -12,7 +12,7 @@ import { flattenMoney, type MoneyRead, moneyPayloadToMinorUnits, rehydrateMoney
|
|
|
12
12
|
const orderEntity: EntityDefinition = createEntity({
|
|
13
13
|
defaultCurrency: "EUR",
|
|
14
14
|
fields: {
|
|
15
|
-
label: createTextField(),
|
|
15
|
+
label: createTextField({ personal: false, reason: "test_fixture" }),
|
|
16
16
|
buyingPrice: createMoneyField(),
|
|
17
17
|
sellingPrice: createMoneyField(),
|
|
18
18
|
},
|
|
@@ -18,8 +18,8 @@ import { setupTestStack, type TestStack, unsafeCreateEntityTable } from "../../s
|
|
|
18
18
|
const linkEntity = createEntity({
|
|
19
19
|
table: "mri_links",
|
|
20
20
|
fields: {
|
|
21
|
-
leftId: createTextField({ required: true }),
|
|
22
|
-
rightId: createTextField({ required: true }),
|
|
21
|
+
leftId: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
22
|
+
rightId: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
const linkTable = buildEntityTable("link", linkEntity);
|
|
@@ -42,7 +42,7 @@ describe("instant() customType is forgiving with ISO strings", () => {
|
|
|
42
42
|
|
|
43
43
|
const tsEntity = createEntity({
|
|
44
44
|
table: "mri_ts",
|
|
45
|
-
fields: { name: createTextField({ required: true }) },
|
|
45
|
+
fields: { name: createTextField({ required: true, personal: false, reason: "test_fixture" }) },
|
|
46
46
|
});
|
|
47
47
|
const tsTable = buildEntityTable("ts-row", tsEntity);
|
|
48
48
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createEntity, createTextField } from "../../engine/factories";
|
|
3
|
+
import { combineClauses, from } from "../../engine/ownership";
|
|
4
|
+
import type { EntityDefinition, SessionUser } from "../../engine/types";
|
|
5
|
+
import { buildParentRefClause } from "../parent-ref-clause";
|
|
6
|
+
import { buildEntityTable } from "../table-builder";
|
|
7
|
+
import type { TenantDb } from "../tenant-db";
|
|
8
|
+
|
|
9
|
+
const TENANT = "11111111-1111-4111-8111-111111111111";
|
|
10
|
+
|
|
11
|
+
const user: SessionUser = {
|
|
12
|
+
id: "22222222-2222-4222-8222-222222222222",
|
|
13
|
+
tenantId: TENANT,
|
|
14
|
+
roles: ["TenantMember"],
|
|
15
|
+
claims: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Only `mode` and `tenantId` are read by the clause builder.
|
|
19
|
+
const tenantDb = { mode: "tenant", tenantId: TENANT } as unknown as TenantDb;
|
|
20
|
+
const systemDb = { mode: "system", tenantId: TENANT } as unknown as TenantDb;
|
|
21
|
+
|
|
22
|
+
const textField = () =>
|
|
23
|
+
createTextField({
|
|
24
|
+
required: true,
|
|
25
|
+
maxLength: 64,
|
|
26
|
+
personal: false,
|
|
27
|
+
reason: "technical_reference",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const joinEntity = createEntity({
|
|
31
|
+
table: "read_test_joins",
|
|
32
|
+
fields: { entityType: textField(), entityId: textField() },
|
|
33
|
+
parentRef: { entityTypeField: "entityType", entityIdField: "entityId" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function hostEntity(opts?: {
|
|
37
|
+
readonly access?: EntityDefinition["access"];
|
|
38
|
+
readonly softDelete?: boolean;
|
|
39
|
+
readonly idType?: "serial" | "uuid";
|
|
40
|
+
readonly parentRef?: EntityDefinition["parentRef"];
|
|
41
|
+
}) {
|
|
42
|
+
return createEntity({
|
|
43
|
+
table: "read_test_hosts",
|
|
44
|
+
fields: { ownerId: textField(), entityType: textField(), entityId: textField() },
|
|
45
|
+
...opts,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const joinTable = buildEntityTable("test-join", joinEntity);
|
|
50
|
+
|
|
51
|
+
function build(
|
|
52
|
+
entities: ReadonlyMap<string, EntityDefinition> | undefined,
|
|
53
|
+
opts: { includeDeleted?: boolean; narrowTypes?: ReadonlySet<string>; db?: TenantDb } = {},
|
|
54
|
+
) {
|
|
55
|
+
return buildParentRefClause(
|
|
56
|
+
joinEntity,
|
|
57
|
+
joinTable,
|
|
58
|
+
"read_test_joins",
|
|
59
|
+
user,
|
|
60
|
+
opts.db ?? tenantDb,
|
|
61
|
+
entities === undefined ? undefined : { entities },
|
|
62
|
+
{
|
|
63
|
+
...(opts.includeDeleted !== undefined && { includeDeleted: opts.includeDeleted }),
|
|
64
|
+
...(opts.narrowTypes !== undefined && { narrowTypes: opts.narrowTypes }),
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("buildParentRefClause", () => {
|
|
70
|
+
test("an entity without parentRef is not gated", () => {
|
|
71
|
+
const plain = createEntity({ table: "read_plain", fields: { a: textField() } });
|
|
72
|
+
const clause = buildParentRefClause(
|
|
73
|
+
plain,
|
|
74
|
+
buildEntityTable("plain", plain),
|
|
75
|
+
"read_plain",
|
|
76
|
+
user,
|
|
77
|
+
tenantDb,
|
|
78
|
+
{ entities: new Map([["host", hostEntity()]]) },
|
|
79
|
+
{},
|
|
80
|
+
);
|
|
81
|
+
expect(clause.kind).toBe("pass");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a raw executor call without the registry option stays ungated", () => {
|
|
85
|
+
expect(build(undefined).kind).toBe("pass");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("no candidate at all denies every row", () => {
|
|
89
|
+
expect(build(new Map()).kind).toBe("empty");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a candidate whose read-ownership can never match is dropped, denying when it was the only one", () => {
|
|
93
|
+
// The role has a rule, but the column it names is not on the host — the
|
|
94
|
+
// ownership clause collapses to `empty`, so the branch cannot be satisfied.
|
|
95
|
+
const host = hostEntity({ access: { read: { TenantMember: from("user:id", "notAColumn") } } });
|
|
96
|
+
expect(build(new Map([["host", host]])).kind).toBe("empty");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("a serial-id candidate is excluded — it has no event-store read path", () => {
|
|
100
|
+
const host = hostEntity({ idType: "serial" });
|
|
101
|
+
expect(build(new Map([["host", host]])).kind).toBe("empty");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a candidate that is itself a join row is excluded — no recursive gating", () => {
|
|
105
|
+
const host = hostEntity({
|
|
106
|
+
parentRef: { entityTypeField: "entityType", entityIdField: "entityId" },
|
|
107
|
+
});
|
|
108
|
+
expect(build(new Map([["host", host]])).kind).toBe("empty");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("narrowTypes restricts the OR-chain to the named hosts", () => {
|
|
112
|
+
const entities = new Map([
|
|
113
|
+
["host", hostEntity()],
|
|
114
|
+
["other", hostEntity()],
|
|
115
|
+
["third", hostEntity()],
|
|
116
|
+
]);
|
|
117
|
+
const all = build(entities);
|
|
118
|
+
const narrowed = build(entities, { narrowTypes: new Set(["other"]) });
|
|
119
|
+
if (all.kind !== "sql" || narrowed.kind !== "sql") throw new Error("expected sql clauses");
|
|
120
|
+
expect(all.sqlText.split(" OR ")).toHaveLength(3);
|
|
121
|
+
expect(narrowed.sqlText.split(" OR ")).toHaveLength(1);
|
|
122
|
+
expect(narrowed.params).toContain("other");
|
|
123
|
+
expect(narrowed.params).not.toContain("host");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("the host's soft-delete filter applies by default and lifts under includeDeleted", () => {
|
|
127
|
+
const entities = new Map([["host", hostEntity({ softDelete: true })]]);
|
|
128
|
+
const gated = build(entities);
|
|
129
|
+
const trash = build(entities, { includeDeleted: true });
|
|
130
|
+
if (gated.kind !== "sql" || trash.kind !== "sql") throw new Error("expected sql clauses");
|
|
131
|
+
expect(gated.sqlText).toContain('"is_deleted" = FALSE');
|
|
132
|
+
expect(trash.sqlText).not.toContain("is_deleted");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the host id is matched through a shape guard, so a foreign id form cannot raise 22P02", () => {
|
|
136
|
+
const clause = build(new Map([["host", hostEntity()]]));
|
|
137
|
+
if (clause.kind !== "sql") throw new Error("expected sql clause");
|
|
138
|
+
expect(clause.sqlText).toContain("CASE WHEN");
|
|
139
|
+
expect(clause.sqlText).toContain("::uuid");
|
|
140
|
+
// The shape itself is bound, never inlined — a literal carrying `$` would
|
|
141
|
+
// be rewritten by shiftParams when the clause is combined.
|
|
142
|
+
expect(clause.params[0]).toBe("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
|
|
143
|
+
expect(clause.sqlText).not.toContain("[0-9a-f]");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("the tenant predicate is bound once and reused across branches", () => {
|
|
147
|
+
const entities = new Map([
|
|
148
|
+
["host", hostEntity()],
|
|
149
|
+
["other", hostEntity()],
|
|
150
|
+
]);
|
|
151
|
+
const clause = build(entities);
|
|
152
|
+
if (clause.kind !== "sql") throw new Error("expected sql clause");
|
|
153
|
+
expect(clause.params.filter((p) => p === TENANT)).toHaveLength(1);
|
|
154
|
+
// uuid shape + 2 tenant params + one host-name param per branch.
|
|
155
|
+
expect(clause.params).toHaveLength(5);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a system-mode db drops the tenant predicate, mirroring list()", () => {
|
|
159
|
+
const clause = build(new Map([["host", hostEntity()]]), { db: systemDb });
|
|
160
|
+
if (clause.kind !== "sql") throw new Error("expected sql clause");
|
|
161
|
+
expect(clause.sqlText).not.toContain("tenant_id");
|
|
162
|
+
expect(clause.params).not.toContain(TENANT);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a dropped branch reserves no placeholder — every $N still binds its own value", () => {
|
|
166
|
+
// First candidate collapses to `empty` (column missing on the host), so its
|
|
167
|
+
// tentatively-reserved indices must not leave a gap for the second.
|
|
168
|
+
const entities = new Map([
|
|
169
|
+
[
|
|
170
|
+
"dropped",
|
|
171
|
+
hostEntity({ access: { read: { TenantMember: from("user:id", "notAColumn") } } }),
|
|
172
|
+
],
|
|
173
|
+
["kept", hostEntity({ access: { read: { TenantMember: from("user:id", "ownerId") } } })],
|
|
174
|
+
]);
|
|
175
|
+
const clause = build(entities);
|
|
176
|
+
if (clause.kind !== "sql") throw new Error("expected sql clause");
|
|
177
|
+
|
|
178
|
+
const indices = [...clause.sqlText.matchAll(/\$(\d+)/g)].map((m) => Number(m[1]));
|
|
179
|
+
expect(Math.max(...indices)).toBeLessThanOrEqual(clause.params.length);
|
|
180
|
+
expect(Math.min(...indices)).toBeGreaterThanOrEqual(1);
|
|
181
|
+
|
|
182
|
+
// The kept branch's host-name param must sit at the placeholder the SQL
|
|
183
|
+
// actually names for it, not one slot over.
|
|
184
|
+
const keptSlot = clause.params.indexOf("kept") + 1;
|
|
185
|
+
expect(keptSlot).toBeGreaterThan(0);
|
|
186
|
+
expect(clause.sqlText).toContain(`"entity_type" = $${keptSlot}`);
|
|
187
|
+
expect(clause.params).toContain(user.id);
|
|
188
|
+
expect(clause.params).not.toContain("dropped");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("combineClauses", () => {
|
|
193
|
+
const sqlA = { kind: "sql", sqlText: '"a" = $1', params: ["av"] } as const;
|
|
194
|
+
const sqlB = { kind: "sql", sqlText: '"b" = $1 OR "c" = $2', params: ["bv", "cv"] } as const;
|
|
195
|
+
|
|
196
|
+
test("empty on either side denies", () => {
|
|
197
|
+
expect(combineClauses({ kind: "empty" }, sqlA).kind).toBe("empty");
|
|
198
|
+
expect(combineClauses(sqlA, { kind: "empty" }).kind).toBe("empty");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("pass is neutral", () => {
|
|
202
|
+
expect(combineClauses({ kind: "pass" }, sqlA)).toEqual(sqlA);
|
|
203
|
+
expect(combineClauses(sqlA, { kind: "pass" })).toEqual(sqlA);
|
|
204
|
+
expect(combineClauses({ kind: "pass" }, { kind: "pass" }).kind).toBe("pass");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("the second clause's placeholders are shifted past the first's params", () => {
|
|
208
|
+
const combined = combineClauses(sqlA, sqlB);
|
|
209
|
+
if (combined.kind !== "sql") throw new Error("expected sql clause");
|
|
210
|
+
expect(combined.sqlText).toBe('("a" = $1 AND "b" = $2 OR "c" = $3)');
|
|
211
|
+
expect(combined.params).toEqual(["av", "bv", "cv"]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -80,8 +80,8 @@ describe("schema migration workflows", () => {
|
|
|
80
80
|
createEntity({
|
|
81
81
|
table: "wf1_posts",
|
|
82
82
|
fields: {
|
|
83
|
-
title: createTextField({ required: true }),
|
|
84
|
-
body: createTextField(),
|
|
83
|
+
title: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
84
|
+
body: createTextField({ personal: false, reason: "test_fixture" }),
|
|
85
85
|
viewCount: createNumberField(),
|
|
86
86
|
publishedAt: createDateField(),
|
|
87
87
|
isDraft: createBooleanField({ default: true }),
|
|
@@ -121,7 +121,7 @@ describe("schema migration workflows", () => {
|
|
|
121
121
|
"article",
|
|
122
122
|
createEntity({
|
|
123
123
|
table: "wf1b_articles",
|
|
124
|
-
fields: { title: createTextField() },
|
|
124
|
+
fields: { title: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
125
125
|
}),
|
|
126
126
|
);
|
|
127
127
|
});
|
|
@@ -138,7 +138,7 @@ describe("schema migration workflows", () => {
|
|
|
138
138
|
// Initial entity with just email
|
|
139
139
|
const initialEntity = createEntity({
|
|
140
140
|
table: "wf2_users",
|
|
141
|
-
fields: { email: createTextField() },
|
|
141
|
+
fields: { email: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
142
142
|
});
|
|
143
143
|
await unsafePushTables(testDb.db, { user: buildEntityTable("user", initialEntity) });
|
|
144
144
|
|
|
@@ -146,8 +146,8 @@ describe("schema migration workflows", () => {
|
|
|
146
146
|
const updatedEntity = createEntity({
|
|
147
147
|
table: "wf2_users",
|
|
148
148
|
fields: {
|
|
149
|
-
email: createTextField(),
|
|
150
|
-
displayName: createTextField(), // NEW FIELD
|
|
149
|
+
email: createTextField({ personal: false, reason: "test_fixture" }),
|
|
150
|
+
displayName: createTextField({ personal: false, reason: "test_fixture" }), // NEW FIELD
|
|
151
151
|
},
|
|
152
152
|
});
|
|
153
153
|
|
|
@@ -168,7 +168,7 @@ describe("schema migration workflows", () => {
|
|
|
168
168
|
// Initial entity with just name
|
|
169
169
|
const initialEntity = createEntity({
|
|
170
170
|
table: "wf3_projects",
|
|
171
|
-
fields: { name: createTextField() },
|
|
171
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
172
172
|
});
|
|
173
173
|
const initialTable = buildEntityTable("project", initialEntity);
|
|
174
174
|
await unsafePushTables(testDb.db, { project: initialTable });
|
|
@@ -182,7 +182,10 @@ describe("schema migration workflows", () => {
|
|
|
182
182
|
// Developer adds boolean field with default
|
|
183
183
|
const updatedEntity = createEntity({
|
|
184
184
|
table: "wf3_projects",
|
|
185
|
-
fields: {
|
|
185
|
+
fields: {
|
|
186
|
+
name: createTextField({ personal: false, reason: "test_fixture" }),
|
|
187
|
+
isArchived: createBooleanField({ default: false }),
|
|
188
|
+
},
|
|
186
189
|
});
|
|
187
190
|
const updatedTable = buildEntityTable("project", updatedEntity);
|
|
188
191
|
await unsafePushTables(testDb.db, { project: updatedTable }, { project: initialTable });
|
|
@@ -202,7 +205,9 @@ describe("schema migration workflows", () => {
|
|
|
202
205
|
// greift.
|
|
203
206
|
const initialEntity = createEntity({
|
|
204
207
|
table: "wf3b_users",
|
|
205
|
-
fields: {
|
|
208
|
+
fields: {
|
|
209
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
210
|
+
},
|
|
206
211
|
});
|
|
207
212
|
const initialTable = buildEntityTable("user", initialEntity);
|
|
208
213
|
await unsafePushTables(testDb.db, { user: initialTable });
|
|
@@ -215,8 +220,13 @@ describe("schema migration workflows", () => {
|
|
|
215
220
|
const updatedEntity = createEntity({
|
|
216
221
|
table: "wf3b_users",
|
|
217
222
|
fields: {
|
|
218
|
-
email: createTextField({ required: true }),
|
|
219
|
-
roles: createTextField({
|
|
223
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
224
|
+
roles: createTextField({
|
|
225
|
+
required: true,
|
|
226
|
+
default: "[]",
|
|
227
|
+
personal: false,
|
|
228
|
+
reason: "test_fixture",
|
|
229
|
+
}),
|
|
220
230
|
},
|
|
221
231
|
});
|
|
222
232
|
const updatedTable = buildEntityTable("user", updatedEntity);
|
|
@@ -232,7 +242,7 @@ describe("schema migration workflows", () => {
|
|
|
232
242
|
"task",
|
|
233
243
|
createEntity({
|
|
234
244
|
table: "wf4_tasks",
|
|
235
|
-
fields: { title: createTextField() },
|
|
245
|
+
fields: { title: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
236
246
|
softDelete: true,
|
|
237
247
|
}),
|
|
238
248
|
);
|
|
@@ -253,7 +263,10 @@ describe("schema migration workflows", () => {
|
|
|
253
263
|
const blogFeature = defineFeature("blog", (r) => {
|
|
254
264
|
r.entity(
|
|
255
265
|
"article",
|
|
256
|
-
createEntity({
|
|
266
|
+
createEntity({
|
|
267
|
+
table: "wf5_articles",
|
|
268
|
+
fields: { title: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
269
|
+
}),
|
|
257
270
|
);
|
|
258
271
|
});
|
|
259
272
|
|
|
@@ -262,7 +275,10 @@ describe("schema migration workflows", () => {
|
|
|
262
275
|
"product",
|
|
263
276
|
createEntity({
|
|
264
277
|
table: "wf5_products",
|
|
265
|
-
fields: {
|
|
278
|
+
fields: {
|
|
279
|
+
name: createTextField({ personal: false, reason: "test_fixture" }),
|
|
280
|
+
price: createNumberField(),
|
|
281
|
+
},
|
|
266
282
|
}),
|
|
267
283
|
);
|
|
268
284
|
});
|
|
@@ -23,7 +23,7 @@ const sourceEntity = createEntity({
|
|
|
23
23
|
table: "ssc_source",
|
|
24
24
|
fields: {
|
|
25
25
|
// `source` collides with the EntityTableMeta discriminator key.
|
|
26
|
-
source: createTextField({ required: true }),
|
|
26
|
+
source: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
29
|
const sourceTable = buildEntityTable("source-row", sourceEntity);
|