@cosmicdrift/kumiko-framework 0.398.0 → 0.400.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 +33 -19
- package/agents/api/dev-server-testing.md +23 -0
- package/agents/api/framework-db.md +277 -0
- package/agents/api/framework-engine.md +1810 -0
- package/agents/api/framework-errors.md +119 -0
- package/agents/api/framework-event-store.md +99 -0
- package/agents/api/framework-seeding.md +26 -0
- package/agents/api/framework-ui-types.md +542 -0
- package/agents/api/renderer-web.md +1450 -0
- package/agents/api/testing-helpers.md +189 -0
- package/agents/api/testing-stack.md +144 -0
- package/agents/api/types.md +5124 -0
- package/agents/api.md +20 -0
- 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.md +3 -0
- package/agents/features-index.md +178 -0
- package/agents/features.md +9 -1
- 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/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 +188 -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 +33 -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 +103 -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 +178 -0
- package/dist/engine/boot-validator/i18n-keys.d.ts +2 -2
- package/dist/engine/boot-validator/i18n-keys.js +1 -3
- 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 +2 -7
- 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 +54 -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 +1 -1
- package/dist/engine/entity-handlers.js +17 -18
- package/dist/engine/entity-list-runtime-options.d.ts +5 -1
- package/dist/engine/entity-list-runtime-options.js +103 -4
- package/dist/engine/factories.d.ts +5 -5
- package/dist/engine/factories.js +3 -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 +2 -1
- package/dist/engine/index.js +2 -1
- 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/entrypoint/index.js +1 -0
- package/dist/entrypoint/internals.d.ts +2 -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 +30 -9
- package/dist/files/index.d.ts +1 -0
- package/dist/files/index.js +1 -0
- package/dist/files/load-save-file-refs.js +3 -4
- 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 +4 -1
- package/dist/files/upload-field-resolution.js +57 -12
- 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-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 +67 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { EnvelopeCipher } from "../../secrets/envelope-cipher.ts";
|
|
2
|
+
export type ValidateBootOptions = {
|
|
3
|
+
/** Warn when an access role is used by exactly one handler/config-key/
|
|
4
|
+
* field across the whole boot scan — often a typo, but also the normal
|
|
5
|
+
* shape of a legitimate fine-grained role (one role scoped to one admin
|
|
6
|
+
* endpoint on purpose). Opt-in (default false): a default-on
|
|
7
|
+
* prod warning that nobody can silence per-role isn't worth the noise
|
|
8
|
+
* it generates on every boot. */
|
|
9
|
+
readonly warnOnUniqueAccessRoles?: boolean;
|
|
10
|
+
/** QNs the app's sidebar allowlist admits. When set, every declared nav
|
|
11
|
+
* whose screen isn't reachable from any allowlisted nav entry gets a
|
|
12
|
+
* boot-time warning — that screen would otherwise be silently
|
|
13
|
+
* unreachable via the sidebar. When omitted and the app has
|
|
14
|
+
* workspaces, the allowlist is derived from `r.workspace({ nav })` and
|
|
15
|
+
* `r.nav({ workspaces })` assignments instead. */
|
|
16
|
+
readonly navAllowlist?: ReadonlySet<string>;
|
|
17
|
+
/** Nav QNs deliberately left out of the sidebar allowlist because their
|
|
18
|
+
* screen is reachable another way (e.g. as a sub-page of a generated
|
|
19
|
+
* settings hub). Suppresses the navAllowlist warning for these QNs. */
|
|
20
|
+
readonly navAllowlistExempt?: ReadonlySet<string>;
|
|
21
|
+
/** Env the encrypted-field master-key probe reads. Defaults to
|
|
22
|
+
* `process.env`; runners pass their resolved env so a ciphertext-only
|
|
23
|
+
* key (already decrypted at boot) is visible to the probe. */
|
|
24
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
25
|
+
/** The cipher the runner injects after validation (built from the app's
|
|
26
|
+
* `masterKey` or the env keyring). Satisfies the availability check for
|
|
27
|
+
* `encrypted: true` fields without an env KEK. */
|
|
28
|
+
readonly entityFieldCipher?: EnvelopeCipher;
|
|
29
|
+
/** Locales every translation key must carry across all mounted features
|
|
30
|
+
* (locale packages count). Without it a key can ship in one language and
|
|
31
|
+
* silently fall back at runtime for users of the others. */
|
|
32
|
+
readonly requiredLocales?: readonly string[];
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BootContext } from "./boot-context.ts";
|
|
2
|
+
import type { FeatureFlags } from "./boot-phases.ts";
|
|
3
|
+
import type { BootReport } from "./boot-report.ts";
|
|
4
|
+
export declare function runAppWidePhase(report: BootReport, ctx: BootContext): void;
|
|
5
|
+
export declare function runEnvironmentPhase(report: BootReport, ctx: BootContext, flags: FeatureFlags): void;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { validateEntityFieldEncryptionAvailable } from "../../db/entity-field-encryption.js";
|
|
2
|
+
import { FILE_STORAGE_PROVIDER_ENV } from "../extension-names.js";
|
|
3
|
+
import { createRegistry } from "../registry.js";
|
|
4
|
+
import { validateProjectionAllowlist } from "../validate-projection-allowlist.js";
|
|
5
|
+
import { runFeatureBootCheck } from "./boot-check.js";
|
|
6
|
+
import { validateCardMetaOverflow } from "./card-meta-overflow.js";
|
|
7
|
+
import { validateConfigReads } from "./config-deps.js";
|
|
8
|
+
import { validateExtensionPreSaveWiring } from "./entity-handler.js";
|
|
9
|
+
import { validateEntityListScreens } from "./entity-list-screens.js";
|
|
10
|
+
import { validateGdprStoragePersistence } from "./gdpr-storage.js";
|
|
11
|
+
import { validateGlobalTenancyEntities } from "./global-tenancy.js";
|
|
12
|
+
import { validateI18nSurfaceKeys, validateRequiredLocales, validateTranslationLocaleConsistency, } from "./i18n-keys.js";
|
|
13
|
+
import { validateNavCycles } from "./nav.js";
|
|
14
|
+
import { validateProjectionListScreens, validateRelatedListSectionQueries, } from "./projection-list-screens.js";
|
|
15
|
+
import { validateQueryOutputColumns } from "./query-output-columns.js";
|
|
16
|
+
import { validateExtensionSelectorPanels } from "./selector-owner-panels.js";
|
|
17
|
+
import { validateSubmitPrefilledHiddenFields } from "./url-prefill-fields.js";
|
|
18
|
+
import { validateDefaultWorkspaceUniqueness } from "./workspaces.js";
|
|
19
|
+
const APP_CHECKS_AFTER_I18N = [
|
|
20
|
+
{
|
|
21
|
+
check: "validateTranslationLocaleConsistency",
|
|
22
|
+
run: (c) => validateTranslationLocaleConsistency(c.features),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
check: "validateRequiredLocales",
|
|
26
|
+
run: (c) => validateRequiredLocales(c.features, c.options?.requiredLocales),
|
|
27
|
+
},
|
|
28
|
+
{ check: "validateEntityListScreens", run: (c) => validateEntityListScreens(c.features) },
|
|
29
|
+
{
|
|
30
|
+
check: "validateSubmitPrefilledHiddenFields",
|
|
31
|
+
run: (c) => validateSubmitPrefilledHiddenFields(c.features),
|
|
32
|
+
},
|
|
33
|
+
{ check: "validateCardMetaOverflow", run: (c) => validateCardMetaOverflow(c.features) },
|
|
34
|
+
{
|
|
35
|
+
check: "validateExtensionSelectorPanels",
|
|
36
|
+
run: (c) => validateExtensionSelectorPanels(c.features),
|
|
37
|
+
},
|
|
38
|
+
// Column/field checks assume every `query` string already resolves (validateQueryRefs, phase 1).
|
|
39
|
+
{ check: "validateQueryOutputColumns", run: (c) => validateQueryOutputColumns(c.features) },
|
|
40
|
+
{
|
|
41
|
+
check: "validateProjectionListScreens",
|
|
42
|
+
run: (c) => validateProjectionListScreens(c.features),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
check: "validateRelatedListSectionQueries",
|
|
46
|
+
run: (c) => validateRelatedListSectionQueries(c.features),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
check: "validateExtensionPreSaveWiring",
|
|
50
|
+
run: (c) => validateExtensionPreSaveWiring(c.features),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
check: "validateGdprStoragePersistence",
|
|
54
|
+
run: (c) => validateGdprStoragePersistence(c.features),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
check: "validateGlobalTenancyEntities",
|
|
58
|
+
run: (c) => validateGlobalTenancyEntities(c.features),
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
function runFeatureBootChecks(report, ctx) {
|
|
62
|
+
for (const feature of ctx.features) {
|
|
63
|
+
for (const check of feature.bootChecks) {
|
|
64
|
+
report.check("runFeatureBootCheck", feature.name, () => runFeatureBootCheck(feature, check, ctx.features));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function runAppWidePhase(report, ctx) {
|
|
69
|
+
const { features } = ctx;
|
|
70
|
+
report.check("validateNavCycles", undefined, () => validateNavCycles(ctx.allNavQns));
|
|
71
|
+
report.check("validateDefaultWorkspaceUniqueness", undefined, () => validateDefaultWorkspaceUniqueness(ctx.allWorkspaceQns));
|
|
72
|
+
const built = {};
|
|
73
|
+
report.check("createRegistry", undefined, () => {
|
|
74
|
+
built.registry = createRegistry(features);
|
|
75
|
+
});
|
|
76
|
+
const registry = built.registry;
|
|
77
|
+
if (registry !== undefined) {
|
|
78
|
+
report.check("validateI18nSurfaceKeys", undefined, () => validateI18nSurfaceKeys(features, registry));
|
|
79
|
+
}
|
|
80
|
+
for (const { check, run } of APP_CHECKS_AFTER_I18N) {
|
|
81
|
+
report.check(check, undefined, () => run(ctx));
|
|
82
|
+
}
|
|
83
|
+
runFeatureBootChecks(report, ctx);
|
|
84
|
+
report.check("validateProjectionAllowlist", undefined, () => validateProjectionAllowlist(features));
|
|
85
|
+
report.check("validateConfigReads", undefined, () => validateConfigReads(features, ctx.allConfigKeys));
|
|
86
|
+
}
|
|
87
|
+
// Independent of the earlier phases: whether the environment can serve the app is
|
|
88
|
+
// a separate question from whether its definitions are consistent.
|
|
89
|
+
export function runEnvironmentPhase(report, ctx, flags) {
|
|
90
|
+
const { options } = ctx;
|
|
91
|
+
if (flags.hasEncryptedFields) {
|
|
92
|
+
// Availability check, not env-presence: eagerly building the keyring
|
|
93
|
+
// catches malformed keys (wrong length, bad base64) at boot instead of
|
|
94
|
+
// on the first encrypted read in prod. An injected cipher (test seam,
|
|
95
|
+
// custom KMS provider) satisfies the requirement without env keys.
|
|
96
|
+
report.check("validateEntityFieldEncryptionAvailable", undefined, () => validateEntityFieldEncryptionAvailable(options?.env, options?.entityFieldCipher));
|
|
97
|
+
}
|
|
98
|
+
report.check("fileStorageProvider", undefined, () => {
|
|
99
|
+
if (flags.hasFileFields && !(options?.env ?? process.env)[FILE_STORAGE_PROVIDER_ENV]) {
|
|
100
|
+
throw new Error(`${FILE_STORAGE_PROVIDER_ENV} environment variable is required (file/image fields in use)`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BootContext } from "./boot-context.ts";
|
|
2
|
+
import type { BootReport } from "./boot-report.ts";
|
|
3
|
+
import { collectDetailForScreens } from "./detail-screens.ts";
|
|
4
|
+
export type FeatureFlags = {
|
|
5
|
+
hasEncryptedFields: boolean;
|
|
6
|
+
hasFileFields: boolean;
|
|
7
|
+
};
|
|
8
|
+
type DetailForScreens = ReturnType<typeof collectDetailForScreens>;
|
|
9
|
+
export declare function runStructurePhase(report: BootReport, ctx: BootContext): DetailForScreens;
|
|
10
|
+
export declare function runFeaturePhase(report: BootReport, ctx: BootContext, detailForScreens: DetailForScreens): FeatureFlags;
|
|
11
|
+
export {};
|
|
@@ -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;
|