@cosmicdrift/kumiko-framework 0.399.0 → 0.401.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 +4 -3
- package/agents/api/framework-engine.md +16 -8
- package/agents/api/framework-ui-types.md +10 -5
- package/agents/api/renderer-web.md +2 -1
- package/agents/api/testing-stack.md +1 -0
- package/agents/api/types.md +256 -80
- package/agents/api.md +3 -3
- package/agents/examples/derived-fields-sql.md +251 -0
- package/agents/examples/documents.md +161 -0
- package/agents/examples/inspection-checklist.md +807 -0
- package/agents/examples/relations.md +13 -3
- package/agents/examples.md +3 -0
- package/agents/features-index.md +2 -0
- package/agents/features.md +8 -0
- package/dist/api/entity-read-access.d.ts +2 -0
- package/dist/api/entity-read-access.js +46 -2
- package/dist/api/server-boot-checks.js +2 -2
- package/dist/api/server-routes.js +3 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/db/derived-field-sql.d.ts +23 -0
- package/dist/db/derived-field-sql.js +115 -0
- package/dist/db/derived-related-sql.d.ts +31 -0
- package/dist/db/derived-related-sql.js +137 -0
- package/dist/db/driver-row-coercion.d.ts +1 -0
- package/dist/db/driver-row-coercion.js +13 -0
- package/dist/db/eagerload.d.ts +3 -1
- package/dist/db/eagerload.js +4 -6
- package/dist/db/event-store-executor-context.js +1 -1
- package/dist/db/event-store-executor-read-paging.js +1 -1
- package/dist/db/event-store-executor-read-search.d.ts +3 -0
- package/dist/db/event-store-executor-read-search.js +17 -9
- package/dist/db/event-store-executor-read-sort.d.ts +9 -2
- package/dist/db/event-store-executor-read-sort.js +27 -14
- package/dist/db/event-store-executor-read-where.d.ts +0 -5
- package/dist/db/event-store-executor-read-where.js +3 -9
- package/dist/db/event-store-executor-read.js +23 -4
- package/dist/db/event-store-executor-write-steps.d.ts +6 -1
- package/dist/db/event-store-executor-write-steps.js +20 -1
- package/dist/db/event-store-executor-write.js +8 -8
- package/dist/db/money.d.ts +4 -0
- package/dist/db/money.js +34 -29
- package/dist/db/parent-ref-clause.d.ts +1 -0
- package/dist/db/parent-ref-clause.js +18 -8
- package/dist/db/read-gate-clause.d.ts +7 -0
- package/dist/db/read-gate-clause.js +201 -0
- package/dist/db/read-ownership.d.ts +9 -0
- package/dist/db/read-ownership.js +12 -0
- package/dist/engine/boot-validator/boot-check.d.ts +3 -1
- package/dist/engine/boot-validator/boot-check.js +7 -11
- package/dist/engine/boot-validator/boot-context.d.ts +26 -0
- package/dist/engine/boot-validator/boot-context.js +57 -0
- package/dist/engine/boot-validator/boot-options.d.ts +38 -0
- package/dist/engine/boot-validator/boot-options.js +1 -0
- package/dist/engine/boot-validator/boot-phases-app.d.ts +5 -0
- package/dist/engine/boot-validator/boot-phases-app.js +107 -0
- package/dist/engine/boot-validator/boot-phases.d.ts +11 -0
- package/dist/engine/boot-validator/boot-phases.js +140 -0
- package/dist/engine/boot-validator/boot-report.d.ts +6 -0
- package/dist/engine/boot-validator/boot-report.js +47 -0
- package/dist/engine/boot-validator/boot-rule.d.ts +12 -0
- package/dist/engine/boot-validator/boot-rule.js +14 -0
- package/dist/engine/boot-validator/boot-warnings.d.ts +2 -0
- package/dist/engine/boot-validator/boot-warnings.js +20 -0
- package/dist/engine/boot-validator/derived-sql-fields.d.ts +4 -0
- package/dist/engine/boot-validator/derived-sql-fields.js +184 -0
- package/dist/engine/boot-validator/entity-handler.d.ts +3 -0
- package/dist/engine/boot-validator/entity-handler.js +6 -3
- package/dist/engine/boot-validator/entity-list-screens.js +3 -1
- package/dist/engine/boot-validator/field-conditions.d.ts +2 -0
- package/dist/engine/boot-validator/field-conditions.js +196 -0
- package/dist/engine/boot-validator/i18n-keys.d.ts +3 -2
- package/dist/engine/boot-validator/i18n-keys.js +18 -4
- package/dist/engine/boot-validator/index.d.ts +5 -34
- package/dist/engine/boot-validator/index.js +24 -204
- package/dist/engine/boot-validator/pii-retention.js +15 -9
- package/dist/engine/boot-validator/projection-list-screens.js +19 -1
- package/dist/engine/boot-validator/read-gate.d.ts +2 -0
- package/dist/engine/boot-validator/read-gate.js +59 -0
- package/dist/engine/boot-validator/screens-forms.js +19 -6
- package/dist/engine/boot-validator/screens-kind-entity-list.js +8 -6
- package/dist/engine/boot-validator/screens-lists.js +8 -0
- package/dist/engine/boot-validator/zod-shape.js +4 -2
- package/dist/engine/build-app-schema.js +11 -5
- package/dist/engine/embedded-derived.d.ts +8 -6
- package/dist/engine/embedded-derived.js +25 -7
- package/dist/engine/entity-handlers.d.ts +13 -6
- package/dist/engine/entity-handlers.js +17 -18
- package/dist/engine/entity-list-runtime-options.d.ts +2 -1
- package/dist/engine/entity-list-runtime-options.js +86 -5
- package/dist/engine/factories.d.ts +18 -6
- package/dist/engine/factories.js +21 -5
- package/dist/engine/field-access.d.ts +2 -0
- package/dist/engine/field-access.js +48 -1
- package/dist/engine/field-state.d.ts +12 -0
- package/dist/engine/field-state.js +67 -0
- package/dist/engine/handler-helpers.d.ts +3 -0
- package/dist/engine/handler-helpers.js +7 -1
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +3 -3
- package/dist/engine/ownership.d.ts +1 -1
- package/dist/engine/ownership.js +9 -5
- package/dist/engine/registry-state.d.ts +1 -0
- package/dist/engine/registry-state.js +1 -1
- package/dist/engine/schema-builder.js +3 -0
- package/dist/engine/strip-unreadable-related-lists.d.ts +4 -0
- package/dist/engine/strip-unreadable-related-lists.js +80 -0
- package/dist/engine/user-entity-reference.d.ts +2 -0
- package/dist/engine/user-entity-reference.js +7 -0
- package/dist/files/assert-file-refs-belong-to-tenant.js +26 -6
- package/dist/files/bind-pending-file-refs.d.ts +3 -2
- package/dist/files/bind-pending-file-refs.js +11 -12
- package/dist/files/bind-referenced-files.write.js +5 -5
- package/dist/files/create-file-ref-from-bytes.d.ts +2 -0
- package/dist/files/create-file-ref-from-bytes.js +2 -0
- package/dist/files/create-file-ref.d.ts +2 -0
- package/dist/files/create-file-ref.js +39 -22
- package/dist/files/dispose-removed-file-refs.js +5 -8
- package/dist/files/file-field-slots.d.ts +10 -0
- package/dist/files/file-field-slots.js +53 -0
- package/dist/files/file-ref-entity.d.ts +1 -1
- package/dist/files/file-ref-table.d.ts +1 -1
- package/dist/files/file-routes.js +12 -3
- package/dist/files/index.d.ts +3 -0
- package/dist/files/index.js +2 -0
- package/dist/files/load-save-file-refs.js +3 -4
- package/dist/files/rebind-file-ref.d.ts +41 -0
- package/dist/files/rebind-file-ref.js +170 -0
- package/dist/files/record-read-check.js +2 -30
- package/dist/files/record-write-check.d.ts +2 -0
- package/dist/files/record-write-check.js +18 -3
- package/dist/files/upload-field-resolution.d.ts +2 -1
- package/dist/files/upload-field-resolution.js +33 -13
- package/dist/i18n/required-surface-keys.d.ts +6 -0
- package/dist/i18n/required-surface-keys.js +61 -0
- package/dist/pipeline/dispatch-batch.js +6 -5
- package/dist/pipeline/dispatch-write.js +2 -1
- package/dist/pipeline/write-result-masking.js +2 -1
- package/dist/schema-cli/validate-boot-input.js +2 -0
- package/dist/schema-cli/validate-command.js +46 -12
- package/dist/schema-cli.js +1 -1
- package/dist/ui-types/index.d.ts +2 -1
- package/dist/ui-types/index.js +2 -1
- package/package.json +3 -3
- package/src/changes.json +68 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { validateAccessDeclarations } from "./access-declarations.js";
|
|
2
|
+
import { validateActionWiring, validateFieldWiring } from "./action-wiring.js";
|
|
3
|
+
import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext.js";
|
|
4
|
+
import { collectExposedApis } from "./boot-context.js";
|
|
5
|
+
import { validateCircularDeps, validateConfigKeyAllowPerRequest, validateConfigKeyBacking, validateConfigKeyBounds, validateConfigKeyComputed, validateConfigKeyOptionsQuery, validateConfigKeyPiiEncrypted, validateConfigKeyRequired, } from "./config-deps.js";
|
|
6
|
+
import { validateSqlDerivedFields } from "./derived-sql-fields.js";
|
|
7
|
+
import { collectDetailForScreens } from "./detail-screens.js";
|
|
8
|
+
import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateEntitySelectAvailabilityQuery, validateEntitySelectConditionalOptions, validateEntitySelectOptionsQuery, validateExtendSchemaCollisions, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateLockedWhen, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateSearchableFieldReadAccess, validateTransitions, } from "./entity-handler.js";
|
|
9
|
+
import { validateFieldConditions } from "./field-conditions.js";
|
|
10
|
+
import { validateHandlerFieldReferences } from "./handler-field-references.js";
|
|
11
|
+
import { validateNavs } from "./nav.js";
|
|
12
|
+
import { validateOwnershipRules } from "./ownership.js";
|
|
13
|
+
import { validateParentRefs } from "./parent-ref.js";
|
|
14
|
+
import { validatePiiAndRetention } from "./pii-retention.js";
|
|
15
|
+
import { validateQueryRefs } from "./query-refs.js";
|
|
16
|
+
import { validateReadGates } from "./read-gate.js";
|
|
17
|
+
import { validateRecordOwnedSubjects } from "./record-owned.js";
|
|
18
|
+
import { validateScreenShortIdCollisions, validateScreens } from "./screens.js";
|
|
19
|
+
import { validateTransferGraph } from "./transfer-graph.js";
|
|
20
|
+
import { validateWorkspaces } from "./workspaces.js";
|
|
21
|
+
function runFeatureChecks(report, checks, feature, ctx) {
|
|
22
|
+
for (const { check, run } of checks) {
|
|
23
|
+
report.check(check, feature.name, () => run(feature, ctx));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Phase 1 breaks the shared lookup tables later checks resolve against; if it
|
|
27
|
+
// finds anything, the later phases would only report follow-up noise.
|
|
28
|
+
const STRUCTURE_FEATURE_CHECKS = [
|
|
29
|
+
{ check: "validateCircularDeps", run: (f, c) => validateCircularDeps(f.name, c.featureMap) },
|
|
30
|
+
{
|
|
31
|
+
check: "validateExtensionUsages",
|
|
32
|
+
run: (f, c) => validateExtensionUsages(f, c.extensionProviders),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
check: "validateReferenceFields",
|
|
36
|
+
run: (f, c) => validateReferenceFields(f, c.featureMap, c.queryHandlerQns),
|
|
37
|
+
},
|
|
38
|
+
{ check: "validateParentRefs", run: (f, c) => validateParentRefs(f, c.featureMap) },
|
|
39
|
+
];
|
|
40
|
+
export function runStructurePhase(report, ctx) {
|
|
41
|
+
const { features, featureMap } = ctx;
|
|
42
|
+
const built = {};
|
|
43
|
+
report.check("validateScreenShortIdCollisions", undefined, () => validateScreenShortIdCollisions(ctx.screensByShortId));
|
|
44
|
+
report.check("collectDetailForScreens", undefined, () => {
|
|
45
|
+
built.detailForScreens = collectDetailForScreens(features, featureMap);
|
|
46
|
+
});
|
|
47
|
+
report.check("validateSqlDerivedFields", undefined, () => validateSqlDerivedFields(features));
|
|
48
|
+
report.check("collectExposedApis", undefined, () => {
|
|
49
|
+
built.exposedApis = collectExposedApis(features);
|
|
50
|
+
});
|
|
51
|
+
report.check("validateQueryRefs", undefined, () => validateQueryRefs(features));
|
|
52
|
+
for (const feature of features) {
|
|
53
|
+
// Without a unique API map the matching would report every API as missing.
|
|
54
|
+
const apis = built.exposedApis;
|
|
55
|
+
if (apis !== undefined) {
|
|
56
|
+
report.check("validateApiExposureMatching", feature.name, () => validateApiExposureMatching(feature, apis, featureMap));
|
|
57
|
+
}
|
|
58
|
+
runFeatureChecks(report, STRUCTURE_FEATURE_CHECKS, feature, ctx);
|
|
59
|
+
}
|
|
60
|
+
report.endPhase();
|
|
61
|
+
return built.detailForScreens ?? new Map();
|
|
62
|
+
}
|
|
63
|
+
const FEATURE_CHECKS = [
|
|
64
|
+
{ check: "validateSearchableFieldReadAccess", run: (f) => validateSearchableFieldReadAccess(f) },
|
|
65
|
+
{ check: "validatePiiAndRetention", run: (f) => validatePiiAndRetention(f) },
|
|
66
|
+
{ check: "validateRecordOwnedSubjects", run: (f) => validateRecordOwnedSubjects(f) },
|
|
67
|
+
{ check: "validateEntitySelectOptionsQuery", run: (f) => validateEntitySelectOptionsQuery(f) },
|
|
68
|
+
{
|
|
69
|
+
check: "validateEntitySelectAvailabilityQuery",
|
|
70
|
+
run: (f, c) => validateEntitySelectAvailabilityQuery(f, c.queryHandlerQns),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
check: "validateEntitySelectConditionalOptions",
|
|
74
|
+
run: (f) => validateEntitySelectConditionalOptions(f),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
check: "validateEmbeddedFields",
|
|
78
|
+
run: (f, c) => validateEmbeddedFields(f, c.featureMap, c.queryHandlerQns),
|
|
79
|
+
},
|
|
80
|
+
{ check: "validateMultiSelectFields", run: (f) => validateMultiSelectFields(f) },
|
|
81
|
+
{ check: "validateImageVariants", run: (f) => validateImageVariants(f) },
|
|
82
|
+
{
|
|
83
|
+
check: "validateHandlerFieldReferences",
|
|
84
|
+
run: (f, c) => validateHandlerFieldReferences(f, c.featureMap),
|
|
85
|
+
},
|
|
86
|
+
{ check: "validateTransitions", run: (f) => validateTransitions(f) },
|
|
87
|
+
{ check: "validateLockedWhen", run: (f) => validateLockedWhen(f) },
|
|
88
|
+
{ check: "validateFieldConditions", run: (f) => validateFieldConditions(f) },
|
|
89
|
+
{ check: "validateExtendSchemaCollisions", run: (f) => validateExtendSchemaCollisions(f) },
|
|
90
|
+
{ check: "validateDerivedFieldCollisions", run: (f) => validateDerivedFieldCollisions(f) },
|
|
91
|
+
{ check: "validateHandlerAccess", run: (f) => validateHandlerAccess(f) },
|
|
92
|
+
{ check: "validateAccessDeclarations", run: (f) => validateAccessDeclarations(f) },
|
|
93
|
+
{ check: "validateLocatedTimestamps", run: (f) => validateLocatedTimestamps(f) },
|
|
94
|
+
{ check: "validateEntityIndexes", run: (f) => validateEntityIndexes(f) },
|
|
95
|
+
{ check: "validateConfigKeyBounds", run: (f) => validateConfigKeyBounds(f) },
|
|
96
|
+
{ check: "validateConfigKeyRequired", run: (f) => validateConfigKeyRequired(f) },
|
|
97
|
+
{ check: "validateConfigKeyComputed", run: (f) => validateConfigKeyComputed(f) },
|
|
98
|
+
{ check: "validateConfigKeyAllowPerRequest", run: (f) => validateConfigKeyAllowPerRequest(f) },
|
|
99
|
+
{ check: "validateConfigKeyBacking", run: (f) => validateConfigKeyBacking(f) },
|
|
100
|
+
{ check: "validateConfigKeyOptionsQuery", run: (f) => validateConfigKeyOptionsQuery(f) },
|
|
101
|
+
{ check: "validateConfigKeyPiiEncrypted", run: (f) => validateConfigKeyPiiEncrypted(f) },
|
|
102
|
+
{
|
|
103
|
+
check: "validateOwnershipRules",
|
|
104
|
+
run: (f, c) => validateOwnershipRules(f, c.allClaimKeys, c.knownRoles),
|
|
105
|
+
},
|
|
106
|
+
{ check: "validateReadGates", run: (f, c) => validateReadGates(f, c.featureMap) },
|
|
107
|
+
{ check: "validateTransferGraph", run: (f, c) => validateTransferGraph(f, c.featureMap) },
|
|
108
|
+
{ check: "validateMultiStreamProjections", run: (f) => validateMultiStreamProjections(f) },
|
|
109
|
+
];
|
|
110
|
+
function validateScreensOfFeature(feature, ctx, detailForScreens) {
|
|
111
|
+
validateScreens(feature, ctx.featureMap, ctx.allWriteHandlerQns, ctx.allScreenQns, ctx.configKeyDefsByQn, ctx.screensByShortId, detailForScreens);
|
|
112
|
+
}
|
|
113
|
+
// validateScreens flags a Function-valued visible/entityId as "unknown field
|
|
114
|
+
// undefined" before the clear wiring message could surface, so a wiring
|
|
115
|
+
// violation in a feature suppresses its screens check.
|
|
116
|
+
function runWiringAndScreens(report, feature, ctx, detailForScreens) {
|
|
117
|
+
const actionWiringOk = report.check("validateActionWiring", feature.name, () => validateActionWiring(feature));
|
|
118
|
+
const fieldWiringOk = report.check("validateFieldWiring", feature.name, () => validateFieldWiring(feature));
|
|
119
|
+
if (actionWiringOk && fieldWiringOk) {
|
|
120
|
+
report.check("validateScreens", feature.name, () => validateScreensOfFeature(feature, ctx, detailForScreens));
|
|
121
|
+
}
|
|
122
|
+
report.check("validateNavs", feature.name, () => validateNavs(feature, ctx.allScreenQns, ctx.allNavQns, ctx.allWorkspaceQns));
|
|
123
|
+
report.check("validateWorkspaces", feature.name, () => validateWorkspaces(feature, ctx.allNavQns));
|
|
124
|
+
}
|
|
125
|
+
export function runFeaturePhase(report, ctx, detailForScreens) {
|
|
126
|
+
const flags = { hasEncryptedFields: false, hasFileFields: false };
|
|
127
|
+
for (const feature of ctx.features) {
|
|
128
|
+
report.check("validateEncryptedFields", feature.name, () => {
|
|
129
|
+
if (validateEncryptedFields(feature))
|
|
130
|
+
flags.hasEncryptedFields = true;
|
|
131
|
+
});
|
|
132
|
+
report.check("validateFileFields", feature.name, () => {
|
|
133
|
+
if (validateFileFields(feature))
|
|
134
|
+
flags.hasFileFields = true;
|
|
135
|
+
});
|
|
136
|
+
runFeatureChecks(report, FEATURE_CHECKS, feature, ctx);
|
|
137
|
+
runWiringAndScreens(report, feature, ctx, detailForScreens);
|
|
138
|
+
}
|
|
139
|
+
return flags;
|
|
140
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BootValidationError } from "./boot-rule.js";
|
|
2
|
+
// A runtime crash is a bug in a validator (or in the input it assumed), not a
|
|
3
|
+
// rule violation: it must surface as itself instead of masquerading as a finding.
|
|
4
|
+
function isRuntimeCrash(thrown) {
|
|
5
|
+
return (!(thrown instanceof Error) ||
|
|
6
|
+
thrown instanceof TypeError ||
|
|
7
|
+
thrown instanceof RangeError ||
|
|
8
|
+
thrown instanceof ReferenceError ||
|
|
9
|
+
thrown instanceof SyntaxError ||
|
|
10
|
+
thrown instanceof EvalError ||
|
|
11
|
+
thrown instanceof URIError);
|
|
12
|
+
}
|
|
13
|
+
export function createBootReport() {
|
|
14
|
+
const violations = [];
|
|
15
|
+
let firstCrash;
|
|
16
|
+
function endPhase() {
|
|
17
|
+
// Crashes are discarded next to violations: they are most likely follow-ups of the broken input.
|
|
18
|
+
if (violations.length > 0)
|
|
19
|
+
throw new BootValidationError([...violations]);
|
|
20
|
+
if (firstCrash !== undefined)
|
|
21
|
+
throw firstCrash.thrown;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
check(checkName, feature, fn) {
|
|
25
|
+
try {
|
|
26
|
+
fn();
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (thrown) {
|
|
30
|
+
if (isRuntimeCrash(thrown)) {
|
|
31
|
+
firstCrash ??= { thrown };
|
|
32
|
+
}
|
|
33
|
+
else if (thrown instanceof Error) {
|
|
34
|
+
violations.push({
|
|
35
|
+
ruleId: `unclassified/${checkName}`,
|
|
36
|
+
check: checkName,
|
|
37
|
+
...(feature === undefined ? {} : { feature }),
|
|
38
|
+
message: thrown.message,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
endPhase,
|
|
45
|
+
finish: endPhase,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type BootViolation = {
|
|
2
|
+
readonly ruleId: string;
|
|
3
|
+
readonly check: string;
|
|
4
|
+
readonly feature?: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly fix?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class BootValidationError extends Error {
|
|
9
|
+
readonly violations: readonly BootViolation[];
|
|
10
|
+
readonly name = "BootValidationError";
|
|
11
|
+
constructor(violations: readonly BootViolation[]);
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function describeViolation(violation) {
|
|
2
|
+
const line = `- [${violation.ruleId}] ${violation.message}`;
|
|
3
|
+
return violation.fix === undefined ? line : `${line}\n Fix: ${violation.fix}`;
|
|
4
|
+
}
|
|
5
|
+
export class BootValidationError extends Error {
|
|
6
|
+
violations;
|
|
7
|
+
name = "BootValidationError";
|
|
8
|
+
constructor(violations) {
|
|
9
|
+
const count = violations.length;
|
|
10
|
+
const header = `Boot validation failed: ${count} violation${count === 1 ? "" : "s"}`;
|
|
11
|
+
super([header, ...violations.map(describeViolation)].join("\n"));
|
|
12
|
+
this.violations = violations;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { warnOnUniqueAccessRoles } from "./access-roles.js";
|
|
2
|
+
import { warnOnToggleableDependencies } from "./config-deps.js";
|
|
3
|
+
import { warnOnNavAccessInversion, warnOnUnreachableNavScreens } from "./nav.js";
|
|
4
|
+
import { warnOnMissingSecurityBaseline } from "./security-baseline.js";
|
|
5
|
+
import { resolveNavAllowlist } from "./workspaces.js";
|
|
6
|
+
// Warnings only make sense for a valid composition, so they run after every
|
|
7
|
+
// validation phase passed.
|
|
8
|
+
export function emitBootWarnings(ctx) {
|
|
9
|
+
const { features, options, allNavQns, allWorkspaceQns } = ctx;
|
|
10
|
+
warnOnNavAccessInversion(allNavQns);
|
|
11
|
+
const effectiveNavAllowlist = resolveNavAllowlist(options?.navAllowlist, allNavQns, allWorkspaceQns);
|
|
12
|
+
if (effectiveNavAllowlist !== undefined) {
|
|
13
|
+
warnOnUnreachableNavScreens(allNavQns, effectiveNavAllowlist, options?.navAllowlistExempt, options?.navAllowlist === undefined ? "workspaces" : "explicit");
|
|
14
|
+
}
|
|
15
|
+
warnOnToggleableDependencies(features, ctx.featureMap);
|
|
16
|
+
warnOnMissingSecurityBaseline(features);
|
|
17
|
+
if (options?.warnOnUniqueAccessRoles === true) {
|
|
18
|
+
warnOnUniqueAccessRoles(features);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type EntityDefinition } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
+
import type { FeatureDefinition } from "../types/index.ts";
|
|
3
|
+
export declare function isSortableSqlDerivedField(entity: EntityDefinition, name: string): boolean;
|
|
4
|
+
export declare function validateSqlDerivedFields(features: readonly FeatureDefinition[]): void;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { isSqlDerivedField, } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
+
import { holdsCiphertextOrSecret } from "../../db/entity-field-encryption.js";
|
|
3
|
+
import { parseRefTargetEntityName } from "../parse-ref-target.js";
|
|
4
|
+
export function isSortableSqlDerivedField(entity, name) {
|
|
5
|
+
const def = entity.derivedFields?.[name];
|
|
6
|
+
return def !== undefined && isSqlDerivedField(def) && def.sortable === true;
|
|
7
|
+
}
|
|
8
|
+
const VALUE_TYPE_OF_FIELD_TYPE = {
|
|
9
|
+
text: "text",
|
|
10
|
+
longText: "text",
|
|
11
|
+
select: "text",
|
|
12
|
+
tz: "text",
|
|
13
|
+
reference: "text",
|
|
14
|
+
number: "number",
|
|
15
|
+
bigInt: "number",
|
|
16
|
+
decimal: "decimal",
|
|
17
|
+
boolean: "boolean",
|
|
18
|
+
date: "date",
|
|
19
|
+
timestamp: "timestamp",
|
|
20
|
+
};
|
|
21
|
+
function valueTypeOfField(entity, field) {
|
|
22
|
+
if (field === "id")
|
|
23
|
+
return "text";
|
|
24
|
+
const def = entity.fields[field];
|
|
25
|
+
return def === undefined ? undefined : VALUE_TYPE_OF_FIELD_TYPE[def.type];
|
|
26
|
+
}
|
|
27
|
+
function requireUsableField(site, entity, field, role) {
|
|
28
|
+
if (field !== "id" && entity.fields[field] === undefined) {
|
|
29
|
+
throw new Error(`${site.where}: ${role} "${field}" is not a field of the entity.`);
|
|
30
|
+
}
|
|
31
|
+
if (valueTypeOfField(entity, field) === undefined) {
|
|
32
|
+
throw new Error(`${site.where}: ${role} "${field}" has type "${entity.fields[field]?.type}", which a SQL derived field cannot read (compound types are not supported; money only as the measure of a related first).`);
|
|
33
|
+
}
|
|
34
|
+
if (holdsCiphertextOrSecret(entity, field)) {
|
|
35
|
+
throw new Error(`${site.where}: ${role} "${field}" is encrypted or secret; a SQL derived field would expose or order by it.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function requireValueType(site, def, expected) {
|
|
39
|
+
if (def.valueType !== expected) {
|
|
40
|
+
throw new Error(`${site.where}: valueType must be "${expected}", got "${def.valueType}".`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function requireMoneyMeasure(site, def, related, measure) {
|
|
44
|
+
if (measure.label === true) {
|
|
45
|
+
throw new Error(`${site.where}: label: true needs a reference field, not money.`);
|
|
46
|
+
}
|
|
47
|
+
if (holdsCiphertextOrSecret(related, measure.field)) {
|
|
48
|
+
throw new Error(`${site.where}: measure field "${measure.field}" is encrypted or secret; a SQL derived field would expose or order by it.`);
|
|
49
|
+
}
|
|
50
|
+
requireUsableField(site, related, measure.orderBy, "measure orderBy");
|
|
51
|
+
requireValueType(site, def, "money");
|
|
52
|
+
}
|
|
53
|
+
function validateCondition(site, related, condition) {
|
|
54
|
+
if ("activeOn" in condition) {
|
|
55
|
+
for (const field of [condition.activeOn.from, condition.activeOn.to]) {
|
|
56
|
+
requireUsableField(site, related, field, "activeOn field");
|
|
57
|
+
if (valueTypeOfField(related, field) !== "date") {
|
|
58
|
+
throw new Error(`${site.where}: activeOn field "${field}" must be a date field.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// skip: activeOn carries no op/value to check
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
requireUsableField(site, related, condition.field, "where field");
|
|
65
|
+
const { value, op } = condition;
|
|
66
|
+
const isList = Array.isArray(value);
|
|
67
|
+
if ((op === "in") !== isList) {
|
|
68
|
+
throw new Error(`${site.where}: where "${condition.field}": op "in" takes a list, and only "in" does.`);
|
|
69
|
+
}
|
|
70
|
+
if (value === null && op !== "eq" && op !== "ne") {
|
|
71
|
+
throw new Error(`${site.where}: where "${condition.field}": null is only valid with eq/ne.`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === "object" && value !== null && !isList) {
|
|
74
|
+
const expected = "today" in value ? "date" : "timestamp";
|
|
75
|
+
if (valueTypeOfField(related, condition.field) !== expected) {
|
|
76
|
+
throw new Error(`${site.where}: where "${condition.field}": ${expected === "date" ? "today" : "now"} needs a ${expected} field.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// A sortable derived value runs its subquery for every row of the page, so the join column of
|
|
81
|
+
// the related table needs an index; buildEntityTable creates none for a plain reference field.
|
|
82
|
+
function requireJoinIndex(site, related, field, relations) {
|
|
83
|
+
const indexed = related.fields[field]?.type === "file" ||
|
|
84
|
+
related.fields[field]?.type === "image" ||
|
|
85
|
+
Object.values(relations ?? {}).some((relation) => relation.type === "belongsTo" && relation.foreignKey === field) ||
|
|
86
|
+
(related.indexes ?? []).some(({ columns }) => {
|
|
87
|
+
const [first, second] = columns;
|
|
88
|
+
return first === field || (first === "tenantId" && second === field);
|
|
89
|
+
});
|
|
90
|
+
if (!indexed) {
|
|
91
|
+
throw new Error(`${site.where}: sortable needs an index on the related table's "${field}"; declare indexes: [{ columns: ["${field}"] }] (or ["tenantId", "${field}"]) on that entity.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function validateRelated(site, own, def, spec, allEntities, relations) {
|
|
95
|
+
const relatedName = parseRefTargetEntityName(spec.entity);
|
|
96
|
+
const related = allEntities.get(relatedName);
|
|
97
|
+
if (related === undefined) {
|
|
98
|
+
throw new Error(`${site.where}: related entity "${spec.entity}" is not registered.`);
|
|
99
|
+
}
|
|
100
|
+
requireUsableField(site, related, spec.on.field, "on.field");
|
|
101
|
+
if (def.sortable === true) {
|
|
102
|
+
requireJoinIndex(site, related, spec.on.field, relations.get(relatedName));
|
|
103
|
+
}
|
|
104
|
+
requireUsableField(site, own, spec.on.outer ?? "id", "on.outer");
|
|
105
|
+
for (const condition of spec.where ?? [])
|
|
106
|
+
validateCondition(site, related, condition);
|
|
107
|
+
requireMeasureValueType(site, def, spec.measure, related);
|
|
108
|
+
}
|
|
109
|
+
function requireMeasureValueType(site, def, measure, related) {
|
|
110
|
+
if (measure.kind === "count") {
|
|
111
|
+
requireValueType(site, def, "number");
|
|
112
|
+
// skip: count measures no field
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (measure.kind === "first" && related.fields[measure.field]?.type === "money") {
|
|
116
|
+
requireMoneyMeasure(site, def, related, measure);
|
|
117
|
+
// skip: money is checked in full above
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
requireUsableField(site, related, measure.field, "measure field");
|
|
121
|
+
if (measure.kind === "countDistinct") {
|
|
122
|
+
requireValueType(site, def, "number");
|
|
123
|
+
// skip: countDistinct has no orderBy or label
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
requireUsableField(site, related, measure.orderBy, "measure orderBy");
|
|
127
|
+
if (measure.label === true) {
|
|
128
|
+
const measured = related.fields[measure.field];
|
|
129
|
+
if (measured?.type !== "reference" || measured.labelField === undefined) {
|
|
130
|
+
throw new Error(`${site.where}: label: true needs a reference field with a labelField.`);
|
|
131
|
+
}
|
|
132
|
+
requireValueType(site, def, "text");
|
|
133
|
+
// skip: a label is always text, the measured field's own type does not apply
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const expected = valueTypeOfField(related, measure.field);
|
|
137
|
+
if (expected !== undefined)
|
|
138
|
+
requireValueType(site, def, expected);
|
|
139
|
+
}
|
|
140
|
+
function validateCoalesce(site, own, def, fields) {
|
|
141
|
+
if (fields.length === 0)
|
|
142
|
+
throw new Error(`${site.where}: coalesce needs at least one field.`);
|
|
143
|
+
const types = new Set();
|
|
144
|
+
for (const field of fields) {
|
|
145
|
+
requireUsableField(site, own, field, "coalesce field");
|
|
146
|
+
types.add(valueTypeOfField(own, field));
|
|
147
|
+
}
|
|
148
|
+
if (types.size !== 1) {
|
|
149
|
+
throw new Error(`${site.where}: coalesce fields must all have the same type.`);
|
|
150
|
+
}
|
|
151
|
+
const [only] = types;
|
|
152
|
+
if (only !== undefined)
|
|
153
|
+
requireValueType(site, def, only);
|
|
154
|
+
}
|
|
155
|
+
export function validateSqlDerivedFields(features) {
|
|
156
|
+
const allEntities = new Map();
|
|
157
|
+
const relations = new Map();
|
|
158
|
+
for (const feature of features) {
|
|
159
|
+
for (const [name, entity] of Object.entries(feature.entities ?? {})) {
|
|
160
|
+
allEntities.set(name, entity);
|
|
161
|
+
const entityRelations = feature.relations[name];
|
|
162
|
+
if (entityRelations !== undefined)
|
|
163
|
+
relations.set(name, entityRelations);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const [entityName, entity] of allEntities) {
|
|
167
|
+
for (const [key, def] of Object.entries(entity.derivedFields ?? {})) {
|
|
168
|
+
const site = { where: `Entity "${entityName}" derived field "${key}"` };
|
|
169
|
+
if (!isSqlDerivedField(def)) {
|
|
170
|
+
if (def.valueType === "money") {
|
|
171
|
+
throw new Error(`${site.where}: valueType "money" needs a sql derived field (related first on a money field); a JS derive has no currency column.`);
|
|
172
|
+
}
|
|
173
|
+
if ("sortable" in def) {
|
|
174
|
+
throw new Error(`${site.where}: sortable needs a sql derived field; a JS derive cannot order in SQL.`);
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (def.sql.kind === "coalesce")
|
|
179
|
+
validateCoalesce(site, entity, def, def.sql.fields);
|
|
180
|
+
else
|
|
181
|
+
validateRelated(site, entity, def, def.sql, allEntities, relations);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { QueryHandlerDef } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import type { FieldCondition } from "@cosmicdrift/kumiko-types/screen";
|
|
2
3
|
import type { FeatureDefinition } from "../types/index.ts";
|
|
3
4
|
export declare const FILE_FIELD_TYPES: Set<string>;
|
|
4
5
|
export declare const PII_DIRECT_NAME_HINTS: ReadonlySet<string>;
|
|
@@ -23,9 +24,11 @@ export declare function validateReferenceFields(feature: FeatureDefinition, feat
|
|
|
23
24
|
export declare function validateEmbeddedFields(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
|
|
24
25
|
export declare function validateEntitySelectOptionsQuery(feature: FeatureDefinition): void;
|
|
25
26
|
export declare function validateEntitySelectAvailabilityQuery(feature: FeatureDefinition, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
|
|
27
|
+
export declare function conditionComparisonValues(when: Exclude<FieldCondition, boolean>): readonly unknown[];
|
|
26
28
|
export declare function validateEntitySelectConditionalOptions(feature: FeatureDefinition): void;
|
|
27
29
|
export declare function validateMultiSelectFields(feature: FeatureDefinition): void;
|
|
28
30
|
export declare function validateImageVariants(feature: FeatureDefinition): void;
|
|
31
|
+
export declare const LOCKABLE_FIELD_TYPES: ReadonlySet<string>;
|
|
29
32
|
export declare function validateLockedWhen(feature: FeatureDefinition): void;
|
|
30
33
|
export declare function validateTransitions(feature: FeatureDefinition): void;
|
|
31
34
|
export declare function validateExtendSchemaCollisions(feature: FeatureDefinition): void;
|
|
@@ -2,6 +2,7 @@ import { SELECT_OPTION_TONES } from "@cosmicdrift/kumiko-types/fields";
|
|
|
2
2
|
import { declaredRateLimitOptions, isRateLimitDisabled } from "@cosmicdrift/kumiko-types/handlers";
|
|
3
3
|
import { ZodObject } from "zod";
|
|
4
4
|
import { VARIANT_NAME_PATTERN } from "../../derivatives/variant-key.js";
|
|
5
|
+
import { holdsFileRefs } from "../../files/file-field-slots.js";
|
|
5
6
|
import { access as accessPresets } from "../config-helpers.js";
|
|
6
7
|
import { parseRefTarget } from "../parse-ref-target.js";
|
|
7
8
|
import { isSelectOptionTone } from "./select-option-tone.js";
|
|
@@ -469,7 +470,7 @@ export function validateEncryptedFields(feature) {
|
|
|
469
470
|
export function validateFileFields(feature) {
|
|
470
471
|
for (const entity of Object.values(feature.entities ?? {})) {
|
|
471
472
|
for (const field of Object.values(entity.fields)) {
|
|
472
|
-
if (
|
|
473
|
+
if (holdsFileRefs(field))
|
|
473
474
|
return true;
|
|
474
475
|
}
|
|
475
476
|
}
|
|
@@ -486,6 +487,8 @@ const VALID_EMBEDDED_SUB_TYPES = new Set([
|
|
|
486
487
|
"select",
|
|
487
488
|
"reference",
|
|
488
489
|
"timestamp",
|
|
490
|
+
"images",
|
|
491
|
+
"files",
|
|
489
492
|
]);
|
|
490
493
|
const NUMERIC_EMBEDDED_SUB_TYPES = new Set(["number", "money", "decimal"]);
|
|
491
494
|
// 15 is where 10^scale exhausts a double's integer range — beyond it the
|
|
@@ -786,7 +789,7 @@ export function validateEntitySelectAvailabilityQuery(feature, queryHandlers) {
|
|
|
786
789
|
}
|
|
787
790
|
}
|
|
788
791
|
}
|
|
789
|
-
function conditionComparisonValues(when) {
|
|
792
|
+
export function conditionComparisonValues(when) {
|
|
790
793
|
if ("eq" in when)
|
|
791
794
|
return [when.eq];
|
|
792
795
|
if ("ne" in when)
|
|
@@ -949,7 +952,7 @@ function isCiphertextAtRest(field) {
|
|
|
949
952
|
// Types whose stored value is a plain scalar that the condition's `===` can match.
|
|
950
953
|
// Compound, temporal, array and bigint columns come back in another shape than the
|
|
951
954
|
// literal an author writes, so such a lock would silently never hold.
|
|
952
|
-
const LOCKABLE_FIELD_TYPES = new Set([
|
|
955
|
+
export const LOCKABLE_FIELD_TYPES = new Set([
|
|
953
956
|
"text",
|
|
954
957
|
"longText",
|
|
955
958
|
"boolean",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LIST_ROW_META_COLUMNS } from "../../ui-types/list-row-meta.js";
|
|
2
2
|
import { normalizeListColumn } from "../screen-helpers.js";
|
|
3
|
+
import { isSortableSqlDerivedField } from "./derived-sql-fields.js";
|
|
3
4
|
/** Operator lists default searchable; low-cardinality audit trails stay opt-out. */
|
|
4
5
|
export const SEARCHABLE_FALSE_WHITELIST = new Set(["download-attempt-list", "cap-list"]);
|
|
5
6
|
// Same set computeListViewModel (headless) can actually render — NOT
|
|
@@ -38,7 +39,8 @@ function validateOneEntityListScreen(feature, screen) {
|
|
|
38
39
|
}
|
|
39
40
|
const entityDef = feature.entities?.[screen.entity];
|
|
40
41
|
const fieldDef = entityDef?.fields[sortField];
|
|
41
|
-
const isSortable =
|
|
42
|
+
const isSortable = (entityDef !== undefined && isSortableSqlDerivedField(entityDef, sortField)) ||
|
|
43
|
+
(fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true);
|
|
42
44
|
if (!isSortable) {
|
|
43
45
|
throw new Error(`${prefix}: defaultSort column "${sortField}" must be sortable`);
|
|
44
46
|
}
|