@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,54 @@
|
|
|
1
|
+
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { FILE_REF_ENTITY_NAME } from "../../files/types.js";
|
|
3
|
+
import { findEntity } from "./parent-ref.js";
|
|
4
|
+
function fail(check, message) {
|
|
5
|
+
throw new Error(`[Kumiko ReadGate] entity "${check.entityName}" (feature: "${check.feature.name}"): ${message}`);
|
|
6
|
+
}
|
|
7
|
+
function validateAnyLink(check, anyLink) {
|
|
8
|
+
const link = findEntity(check.featureMap, anyLink.entity);
|
|
9
|
+
if (link === undefined) {
|
|
10
|
+
fail(check, `readGate.anyLink.entity "${anyLink.entity}" is not registered.`);
|
|
11
|
+
}
|
|
12
|
+
if (link.parentRef === undefined) {
|
|
13
|
+
fail(check, `readGate.anyLink.entity "${anyLink.entity}" must declare a parentRef; its host visibility is what the gate checks.`);
|
|
14
|
+
}
|
|
15
|
+
if (link.fields[anyLink.field] === undefined) {
|
|
16
|
+
fail(check, `readGate.anyLink.field "${anyLink.field}" is not a field of "${anyLink.entity}".`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function validateFileOwner(check, fileOwner) {
|
|
20
|
+
const fileField = check.entity.fields[fileOwner.field];
|
|
21
|
+
if (fileField?.type !== "file" && fileField?.type !== "image") {
|
|
22
|
+
fail(check, `readGate.fileOwner.field "${fileOwner.field}" must be a single file or image field.`);
|
|
23
|
+
}
|
|
24
|
+
if (findEntity(check.featureMap, FILE_REF_ENTITY_NAME) === undefined) {
|
|
25
|
+
fail(check, `readGate.fileOwner needs the files feature (entity "${FILE_REF_ENTITY_NAME}").`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function validateNoHandWrittenReaders(check) {
|
|
29
|
+
for (const verb of ["list", "detail"]) {
|
|
30
|
+
const handlerName = `${check.entityName}:${verb}`;
|
|
31
|
+
for (const f of check.featureMap.values()) {
|
|
32
|
+
const handler = f.queryHandlers[handlerName];
|
|
33
|
+
if (handler !== undefined && handler[ENTITY_CONVENTION_QUERY_BRAND] !== true) {
|
|
34
|
+
fail(check, `query handler "${handlerName}" is hand-written and would bypass the readGate. Fix: register it via defineEntityListHandler/defineEntityDetailHandler.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function validateReadGates(feature, featureMap) {
|
|
40
|
+
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
41
|
+
const gate = entity.readGate;
|
|
42
|
+
if (gate === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
const check = { feature, featureMap, entityName, entity };
|
|
45
|
+
if (entity.parentRef !== undefined) {
|
|
46
|
+
fail(check, "readGate and parentRef cannot be combined; a gated entity cannot be a join row.");
|
|
47
|
+
}
|
|
48
|
+
if (gate.anyLink !== undefined)
|
|
49
|
+
validateAnyLink(check, gate.anyLink);
|
|
50
|
+
if (gate.fileOwner !== undefined)
|
|
51
|
+
validateFileOwner(check, gate.fileOwner);
|
|
52
|
+
validateNoHandWrittenReaders(check);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -3,6 +3,22 @@ import { isKebabSegment } from "../qualified-name.js";
|
|
|
3
3
|
import { isExtensionEditSection, isWriteFormEditSection, normalizeEditField, sectionFieldSpecs, } from "../screen-helpers.js";
|
|
4
4
|
import { validateFormDerivations, validateFormDerivationsEnforced } from "./form-derivations.js";
|
|
5
5
|
import { assertWriteHandlerRegistered, isSensitiveOrPasswordField, validateIdlessSuccessLink, validateRedirectTarget, validateScreenNavTarget, } from "./screens-actions.js";
|
|
6
|
+
function assertKnownSubFields(where, option, cells, schema) {
|
|
7
|
+
for (const cell of cells ?? []) {
|
|
8
|
+
if (schema[cell] === undefined) {
|
|
9
|
+
throw new Error(`${where} embeddedList.${option} names "${cell}", which is not a sub-field ` +
|
|
10
|
+
`(known: ${Object.keys(schema).sort().join(", ")}).`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function assertCardHiddenCellsReadOnly(where, cardHiddenCells, readOnlyCells) {
|
|
15
|
+
for (const cell of cardHiddenCells ?? []) {
|
|
16
|
+
if (readOnlyCells?.includes(cell) !== true) {
|
|
17
|
+
throw new Error(`${where} embeddedList.cardHiddenCells names "${cell}", which is editable; ` +
|
|
18
|
+
`only read-only cells (readOnlyCells) can be hidden on cards, otherwise phone users cannot fill it.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
6
22
|
// A field type in NO_WIDGET_FIELD_TYPES renders read-only on the auto-wired
|
|
7
23
|
// entityEdit path — a required field the user can never fill would
|
|
8
24
|
// block every save. Only the statically-resolvable case is caught here: a
|
|
@@ -23,12 +39,9 @@ export function validateEditFieldExtras(featureName, screenId, entityDef, fieldS
|
|
|
23
39
|
throw new Error(`${where} sets embeddedList, which only applies to an embedded list ` +
|
|
24
40
|
`(createEmbeddedListField); the field is type "${fieldDef.type}". Remove embeddedList or point it at an embedded list field.`);
|
|
25
41
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`(known: ${Object.keys(fieldDef.schema).sort().join(", ")}).`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
42
|
+
assertKnownSubFields(where, "readOnlyCells", listSpec.readOnlyCells, fieldDef.schema);
|
|
43
|
+
assertKnownSubFields(where, "cardHiddenCells", listSpec.cardHiddenCells, fieldDef.schema);
|
|
44
|
+
assertCardHiddenCellsReadOnly(where, listSpec.cardHiddenCells, listSpec.readOnlyCells);
|
|
32
45
|
}
|
|
33
46
|
const prefill = fieldSpec.prefill;
|
|
34
47
|
// skip: no prefill declared
|
|
@@ -3,6 +3,7 @@ import { rowMetaFieldNames } from "../../db/table-builder.js";
|
|
|
3
3
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
4
4
|
import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops.js";
|
|
5
5
|
import { normalizeListColumn } from "../screen-helpers.js";
|
|
6
|
+
import { isSortableSqlDerivedField } from "./derived-sql-fields.js";
|
|
6
7
|
import { validateEntityListAggregateOptions } from "./entity-list-aggregate-options.js";
|
|
7
8
|
import { LIST_ROW_META_COLUMN_NAMES } from "./entity-list-screens.js";
|
|
8
9
|
import { validateListBulkActions } from "./list-bulk-actions.js";
|
|
@@ -14,10 +15,9 @@ import { validateRelatedListSection, validateRowActionTarget } from "./screens-l
|
|
|
14
15
|
function resolveEntityListFields(ctx, screenId, entityName) {
|
|
15
16
|
const entityDef = resolveScreenEntity(ctx, screenId, entityName);
|
|
16
17
|
const fieldNames = new Set(Object.keys(entityDef.fields));
|
|
17
|
-
// List columns may also name a read-time derived field (not a stored
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// a non-column is a silent no-op (see DerivedFieldDef).
|
|
18
|
+
// List columns may also name a read-time derived field (not a stored column).
|
|
19
|
+
// Deliberately NOT added to `fieldNames`, so filtering one still fails; only a
|
|
20
|
+
// `sortable` SQL derived field may be the defaultSort (see isSortableSqlDerivedField).
|
|
21
21
|
const columnFieldNames = entityDef.derivedFields !== undefined
|
|
22
22
|
? new Set([...fieldNames, ...Object.keys(entityDef.derivedFields)])
|
|
23
23
|
: fieldNames;
|
|
@@ -52,7 +52,8 @@ function validateEntityListColumns(ctx, screenId, screen, fields) {
|
|
|
52
52
|
}
|
|
53
53
|
function validateEntityListDefaultSort(featureName, screenId, defaultSort, fields) {
|
|
54
54
|
const sortField = defaultSort.field;
|
|
55
|
-
|
|
55
|
+
const sortsOnDerived = isSortableSqlDerivedField(fields.entityDef, sortField);
|
|
56
|
+
if (!fields.fieldNames.has(sortField) && !sortsOnDerived) {
|
|
56
57
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) defaultSort references unknown ` +
|
|
57
58
|
`field "${sortField}". Known fields: ${[...fields.fieldNames].sort().join(", ")}`);
|
|
58
59
|
}
|
|
@@ -64,7 +65,8 @@ function validateEntityListDefaultSort(featureName, screenId, defaultSort, field
|
|
|
64
65
|
// Number/Date fields are naturally sortable. Authors can mark them
|
|
65
66
|
// sortable when their field type supports it.
|
|
66
67
|
const fieldDef = fields.entityDef.fields[sortField];
|
|
67
|
-
const isSortable =
|
|
68
|
+
const isSortable = sortsOnDerived ||
|
|
69
|
+
(fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true);
|
|
68
70
|
if (!isSortable) {
|
|
69
71
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) defaultSort.field "${sortField}" ` +
|
|
70
72
|
`is not sortable. Set sortable: true on the field definition or pick another field.`);
|
|
@@ -84,11 +84,19 @@ function validateRelatedListTableAndBulk(args) {
|
|
|
84
84
|
validateRelatedListRowActions(args, section.rowActions);
|
|
85
85
|
validateListBulkActions(`[Feature ${featureName}] ${where}`, section.bulkActions, section.rowActions);
|
|
86
86
|
}
|
|
87
|
+
function assertReadOnlyHasNoRowInteraction(args) {
|
|
88
|
+
const { section } = args;
|
|
89
|
+
const interactive = section.rowClick !== undefined || (section.rowActions?.length ?? 0) > 0;
|
|
90
|
+
if (section.readOnly === true && interactive) {
|
|
91
|
+
throw new Error(`[Feature ${args.featureName}] ${args.where} is readOnly but declares rowClick/rowActions. Remove readOnly or the row interaction.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
87
94
|
export function validateRelatedListSection(args) {
|
|
88
95
|
const { featureName, featureMap, where, section } = args;
|
|
89
96
|
if (!section.query || typeof section.query !== "string") {
|
|
90
97
|
throw new Error(`[Feature ${featureName}] ${where} has empty or non-string query. Set query to a registered query name.`);
|
|
91
98
|
}
|
|
99
|
+
assertReadOnlyHasNoRowInteraction(args);
|
|
92
100
|
if (section.entity !== undefined) {
|
|
93
101
|
assertRefTargetRegistered(`[Feature ${featureName}] ${where}`, "entity", section.entity, featureName, featureMap);
|
|
94
102
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ZodArray, ZodCatch, ZodDefault, ZodIntersection, ZodLazy, ZodMap, ZodNonOptional, ZodNullable, ZodObject, ZodOptional, ZodPipe, ZodPrefault, ZodPromise, ZodReadonly, ZodRecord, ZodSet, ZodTuple, ZodUnion, ZodXor, } from "zod";
|
|
1
|
+
import { ZodArray, ZodCatch, ZodDefault, ZodIntersection, ZodLazy, ZodMap, ZodNever, ZodNonOptional, ZodNullable, ZodObject, ZodOptional, ZodPipe, ZodPrefault, ZodPromise, ZodReadonly, ZodRecord, ZodSet, ZodTuple, ZodUnion, ZodXor, } from "zod";
|
|
2
2
|
// Drills through wrapper types (.nullable(), .optional(), .default()) a
|
|
3
3
|
// handler's schema may use around its actual object/array shape — e.g. a
|
|
4
4
|
// detail query returning `Row | null` when the record doesn't exist.
|
|
@@ -29,7 +29,9 @@ export function getZodObjectShape(schema) {
|
|
|
29
29
|
const unwrapped = unwrapZodType(schema);
|
|
30
30
|
// loose()/catchall() objects accept keys beyond the declared shape, so the
|
|
31
31
|
// declared keys are not the full set callers may legitimately reference.
|
|
32
|
-
|
|
32
|
+
// .strict() also sets a catchall, ZodNever, which closes the keys instead.
|
|
33
|
+
const catchall = unwrapped instanceof ZodObject ? unwrapped.def.catchall : undefined;
|
|
34
|
+
const hasOpenKeys = catchall !== undefined && !(catchall instanceof ZodNever);
|
|
33
35
|
return unwrapped instanceof ZodObject && !hasOpenKeys ? unwrapped.shape : undefined;
|
|
34
36
|
}
|
|
35
37
|
// The shape of one row for a query whose result follows the paged-list
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// Feature toggles: this schema is tenant-independent and lists every feature.
|
|
26
26
|
// The schema route strips features the caller's tenant has switched off
|
|
27
27
|
// (r.toggleable) via stripDisabledFeaturesFromAppSchema.
|
|
28
|
+
import { isSqlDerivedField } from "@cosmicdrift/kumiko-types/fields";
|
|
28
29
|
import { ZodObject } from "zod";
|
|
29
30
|
import { createFallbackLogger } from "../logging/utils.js";
|
|
30
31
|
import { collectUrlPrefillFieldsByScreenQn } from "./boot-validator/url-prefill-fields.js";
|
|
@@ -426,12 +427,13 @@ function projectEntity(entityName, entity) {
|
|
|
426
427
|
...(entity.lockedWhen !== undefined && { lockedWhen: entity.lockedWhen }),
|
|
427
428
|
};
|
|
428
429
|
}
|
|
429
|
-
//
|
|
430
|
-
// JSON-safe (it would otherwise trigger the output-walk guard). Return
|
|
431
|
-
// ClientDerivedFieldDef honestly instead of pretending to return a
|
|
432
|
-
// DerivedFieldDef without its derive function.
|
|
430
|
+
// derive/sql are server-only and not JSON-safe (they would trip the output-walk guard).
|
|
433
431
|
function projectDerivedField(derivedDef) {
|
|
434
|
-
return {
|
|
432
|
+
return {
|
|
433
|
+
valueType: derivedDef.valueType,
|
|
434
|
+
...(isSqlDerivedField(derivedDef) &&
|
|
435
|
+
derivedDef.sortable === true && { sortable: true }),
|
|
436
|
+
};
|
|
435
437
|
}
|
|
436
438
|
// A textarea height that is not a positive integer cannot render anywhere; fail
|
|
437
439
|
// at boot with the field name instead of letting each renderer guess a fallback.
|
|
@@ -491,6 +493,9 @@ const FORWARDED_FIELD_PROPERTIES = [
|
|
|
491
493
|
["optionsAvailabilityQuery", isStringValue],
|
|
492
494
|
// Select: options only offered while a sibling-field condition holds; the renderer filters, the write path enforces.
|
|
493
495
|
["conditionalOptions", Array.isArray],
|
|
496
|
+
// Conditional field state: the renderer evaluates it per row/form, the write path enforces requiredWhen.
|
|
497
|
+
["visibleWhen", isJsonSafeObject],
|
|
498
|
+
["requiredWhen", isJsonSafeObject],
|
|
494
499
|
["multiple", isBooleanValue],
|
|
495
500
|
// MultiSelect: display picks checkboxes vs. combobox in the renderer,
|
|
496
501
|
// columns/maxRows size the checkbox grid — without these the renderer
|
|
@@ -527,6 +532,7 @@ const FORWARDED_FIELD_PROPERTIES = [
|
|
|
527
532
|
// the preview variant.
|
|
528
533
|
["accept", isJsonSafeArray],
|
|
529
534
|
["maxSize", isStringValue],
|
|
535
|
+
["maxCount", isNumberValue],
|
|
530
536
|
["variants", isJsonSafeObject],
|
|
531
537
|
// decimal, incl. embedded sub-fields: rounding of derived embedded-list cells.
|
|
532
538
|
["scale", isNumberValue],
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { EmbeddedDerivedCellDef, EmbeddedSubFieldDef } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
+
export type NumericDerivedOp = Exclude<EmbeddedDerivedCellDef["op"], "inRange">;
|
|
2
3
|
/** Computes a derived cell from its source values. Missing/non-numeric
|
|
3
4
|
* sources: "multiply"/"percent" with any missing source → undefined; "sum"/"subtract"
|
|
4
5
|
* treat missing as 0 only when at least one source is present — if every
|
|
5
6
|
* source is missing, return undefined (do not invent 0). Money cells are
|
|
6
7
|
* minor-unit integers — unit-agnostic arithmetic; `withDerivedCells` rounds
|
|
7
8
|
* to the target sub-field's declared precision afterward. */
|
|
8
|
-
export declare function computeDerivedCellValue(op:
|
|
9
|
+
export declare function computeDerivedCellValue(op: NumericDerivedOp, values: readonly (number | undefined)[]): number | undefined;
|
|
9
10
|
export type DerivedCellRoundingTarget = {
|
|
10
11
|
readonly type: EmbeddedSubFieldDef["type"];
|
|
11
12
|
readonly scale?: number;
|
|
@@ -19,11 +20,12 @@ export declare function roundDerivedCellValue(value: number, target: DerivedCell
|
|
|
19
20
|
/** Recomputes every derived cell of an embedded-list row from its raw
|
|
20
21
|
* values, overwriting whatever the client sent instead of merely checking
|
|
21
22
|
* it — the server is the authority for derived cells. Reads source values
|
|
22
|
-
* from the working copy so
|
|
23
|
-
*
|
|
23
|
+
* from the working copy, so a cell declared after its source sees the
|
|
24
|
+
* recomputed source. A row that isn't a
|
|
24
25
|
* plain object (already invalid, or not this field's shape) passes through
|
|
25
26
|
* untouched — validation downstream rejects it. The computed value is
|
|
26
|
-
* rounded to the target sub-field's declared precision (`
|
|
27
|
+
* rounded to the target sub-field's declared precision (`targets`) before
|
|
27
28
|
* it's written back, so a fractional product lands on a value the target
|
|
28
|
-
* type can actually represent.
|
|
29
|
-
|
|
29
|
+
* type can actually represent. A cell whose `when` is false for the row is
|
|
30
|
+
* not derived there: its row value is left as sent. */
|
|
31
|
+
export declare function withDerivedCells(row: unknown, derived: Readonly<Record<string, EmbeddedDerivedCellDef>>, targets: Readonly<Record<string, DerivedCellRoundingTarget>>): unknown;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { evalFieldCondition } from "./screen-helpers.js";
|
|
2
3
|
/** Computes a derived cell from its source values. Missing/non-numeric
|
|
3
4
|
* sources: "multiply"/"percent" with any missing source → undefined; "sum"/"subtract"
|
|
4
5
|
* treat missing as 0 only when at least one source is present — if every
|
|
@@ -58,33 +59,50 @@ function roundHalfAwayFromZero(value, decimals) {
|
|
|
58
59
|
const scaled = Number((Math.abs(value) * factor).toPrecision(15));
|
|
59
60
|
return (Math.sign(value) * Math.round(scaled)) / factor;
|
|
60
61
|
}
|
|
62
|
+
/** `from` is `[value, min, max]`; bounds are inclusive and either may be missing. */
|
|
63
|
+
function computeInRangeCell(values, inside, outside) {
|
|
64
|
+
const [value, min, max] = values;
|
|
65
|
+
if (value === undefined || (min === undefined && max === undefined))
|
|
66
|
+
return undefined;
|
|
67
|
+
const aboveMin = min === undefined || value >= min;
|
|
68
|
+
const belowMax = max === undefined || value <= max;
|
|
69
|
+
return aboveMin && belowMax ? inside : outside;
|
|
70
|
+
}
|
|
61
71
|
/** Recomputes every derived cell of an embedded-list row from its raw
|
|
62
72
|
* values, overwriting whatever the client sent instead of merely checking
|
|
63
73
|
* it — the server is the authority for derived cells. Reads source values
|
|
64
|
-
* from the working copy so
|
|
65
|
-
*
|
|
74
|
+
* from the working copy, so a cell declared after its source sees the
|
|
75
|
+
* recomputed source. A row that isn't a
|
|
66
76
|
* plain object (already invalid, or not this field's shape) passes through
|
|
67
77
|
* untouched — validation downstream rejects it. The computed value is
|
|
68
|
-
* rounded to the target sub-field's declared precision (`
|
|
78
|
+
* rounded to the target sub-field's declared precision (`targets`) before
|
|
69
79
|
* it's written back, so a fractional product lands on a value the target
|
|
70
|
-
* type can actually represent.
|
|
71
|
-
|
|
80
|
+
* type can actually represent. A cell whose `when` is false for the row is
|
|
81
|
+
* not derived there: its row value is left as sent. */
|
|
82
|
+
export function withDerivedCells(row, derived, targets) {
|
|
72
83
|
if (!isPlainObject(row))
|
|
73
84
|
return row;
|
|
74
85
|
const copy = { ...row };
|
|
75
86
|
for (const [cellName, def] of Object.entries(derived)) {
|
|
87
|
+
if (def.when !== undefined && !evalFieldCondition(def.when, row))
|
|
88
|
+
continue;
|
|
76
89
|
const sourceValues = def.from.map((sourceField) => {
|
|
77
90
|
const value = copy[sourceField];
|
|
78
91
|
return typeof value === "number" ? value : undefined;
|
|
79
92
|
});
|
|
80
|
-
const computed =
|
|
93
|
+
const computed = def.op === "inRange"
|
|
94
|
+
? computeInRangeCell(sourceValues, def.inside, def.outside)
|
|
95
|
+
: computeDerivedCellValue(def.op, sourceValues);
|
|
81
96
|
if (computed === undefined) {
|
|
82
97
|
// Always clear — keeping a client value would let callers spoof required
|
|
83
98
|
// derived cells when sources are missing (server must stay authoritative).
|
|
84
99
|
delete copy[cellName];
|
|
85
100
|
}
|
|
101
|
+
else if (typeof computed === "string") {
|
|
102
|
+
copy[cellName] = computed;
|
|
103
|
+
}
|
|
86
104
|
else {
|
|
87
|
-
const target =
|
|
105
|
+
const target = targets[cellName];
|
|
88
106
|
copy[cellName] = target === undefined ? computed : roundDerivedCellValue(computed, target);
|
|
89
107
|
}
|
|
90
108
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EntityDefinition } from "@cosmicdrift/kumiko-types/fields";
|
|
2
2
|
import type { AccessRule, EscapeHatchDeclaration, QueryHandlerDef, WriteHandlerDef } from "@cosmicdrift/kumiko-types/handlers";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { type EventStoreExecutor } from "../db/event-store-executor.ts";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSqlDerivedField, } from "@cosmicdrift/kumiko-types/fields";
|
|
1
2
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
3
|
import { listAggregatePayloadSchema, } from "@cosmicdrift/kumiko-types/list-aggregate-types";
|
|
3
4
|
import * as z from "zod";
|
|
@@ -10,7 +11,7 @@ import { InternalError } from "../errors/index.js";
|
|
|
10
11
|
import { isSystemIdentity } from "../pipeline/system-identity-switch.js";
|
|
11
12
|
import { assertUnreachable } from "../utils/index.js";
|
|
12
13
|
import { PAGED_QUERY_HANDLER_BRAND } from "./define-handler.js";
|
|
13
|
-
import { entityListRuntime } from "./entity-list-runtime-options.js";
|
|
14
|
+
import { entityListRuntime, parentVisibilityFor } from "./entity-list-runtime-options.js";
|
|
14
15
|
import { assertReason } from "./escape-hatch-reason.js";
|
|
15
16
|
import { dropEmptyWriteOnlyValues } from "./field-access.js";
|
|
16
17
|
import { instructionFieldNames } from "./instruction-fields.js";
|
|
@@ -289,7 +290,7 @@ export function defineEntityWriteHandler(name, entity, options) {
|
|
|
289
290
|
// they rely on to intentionally force a fresh event/ciphertext.
|
|
290
291
|
return executor.update(payload, event.user, db, {
|
|
291
292
|
streamTenantId,
|
|
292
|
-
parentVisibility:
|
|
293
|
+
parentVisibility: parentVisibilityFor(ctx, event.user),
|
|
293
294
|
skipUnchanged: true,
|
|
294
295
|
preSave: runPreSave &&
|
|
295
296
|
((changes, previous, isNew) => runPreSave(event.type, changes, previous, isNew)),
|
|
@@ -304,7 +305,7 @@ export function defineEntityWriteHandler(name, entity, options) {
|
|
|
304
305
|
const streamTenantId = await foreignRowTenantFor(event.user, payload.id, db, ctx);
|
|
305
306
|
return executor.delete(payload, event.user, db, {
|
|
306
307
|
streamTenantId,
|
|
307
|
-
parentVisibility:
|
|
308
|
+
parentVisibility: parentVisibilityFor(ctx, event.user),
|
|
308
309
|
});
|
|
309
310
|
};
|
|
310
311
|
break;
|
|
@@ -316,7 +317,7 @@ export function defineEntityWriteHandler(name, entity, options) {
|
|
|
316
317
|
const streamTenantId = await foreignRowTenantFor(event.user, payload.id, db, ctx);
|
|
317
318
|
return executor.restore(payload, event.user, db, {
|
|
318
319
|
streamTenantId,
|
|
319
|
-
parentVisibility:
|
|
320
|
+
parentVisibility: parentVisibilityFor(ctx, event.user),
|
|
320
321
|
});
|
|
321
322
|
};
|
|
322
323
|
break;
|
|
@@ -334,23 +335,21 @@ export function defineEntityWriteHandler(name, entity, options) {
|
|
|
334
335
|
...(excludedFields.length > 0 && { excludedFields }),
|
|
335
336
|
};
|
|
336
337
|
}
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
// rows untouched) when the entity declares no derived fields.
|
|
342
|
-
function augmentDerivedFields(rows, entity) {
|
|
338
|
+
// Runs the JS `derive` fields on the row shape the view-model will see (after the SQL fetch,
|
|
339
|
+
// reference eagerload and SQL derived values), so a JS derive may read a SQL derived value.
|
|
340
|
+
// Clock comes from the handler's tz context, never Temporal.Now.
|
|
341
|
+
function augmentDerivedFields(rows, entity, asOf) {
|
|
343
342
|
const derived = entity.derivedFields;
|
|
344
343
|
if (derived === undefined)
|
|
345
344
|
return rows;
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
345
|
+
const jsEntries = Object.entries(derived).flatMap(([fieldName, def]) => isSqlDerivedField(def) ? [] : [[fieldName, def.derive]]);
|
|
346
|
+
if (jsEntries.length === 0)
|
|
348
347
|
return rows;
|
|
349
|
-
const ctx = { asOf
|
|
348
|
+
const ctx = { asOf };
|
|
350
349
|
return rows.map((row) => {
|
|
351
350
|
const out = { ...row };
|
|
352
|
-
for (const [fieldName,
|
|
353
|
-
out[fieldName] =
|
|
351
|
+
for (const [fieldName, derive] of jsEntries) {
|
|
352
|
+
out[fieldName] = derive(row, ctx);
|
|
354
353
|
}
|
|
355
354
|
return out;
|
|
356
355
|
});
|
|
@@ -409,7 +408,7 @@ export function defineEntityQueryHandler(name, entity, options) {
|
|
|
409
408
|
const enrichedRows = hasRefFields
|
|
410
409
|
? await enrichWithReferences(result.rows, entity, resolveEntity, db, eagerLoadViewer)
|
|
411
410
|
: result.rows;
|
|
412
|
-
return { ...result, rows: augmentDerivedFields(enrichedRows, entity) };
|
|
411
|
+
return { ...result, rows: augmentDerivedFields(enrichedRows, entity, ctx.tz.now()) };
|
|
413
412
|
};
|
|
414
413
|
break;
|
|
415
414
|
case "detail":
|
|
@@ -417,13 +416,13 @@ export function defineEntityQueryHandler(name, entity, options) {
|
|
|
417
416
|
handler = async (query, ctx) => {
|
|
418
417
|
const db = dbFor(ctx);
|
|
419
418
|
const row = await executor.detail(query.payload, query.user, db, {
|
|
420
|
-
parentVisibility:
|
|
419
|
+
parentVisibility: parentVisibilityFor(ctx, query.user),
|
|
421
420
|
}); // @cast-boundary engine-payload
|
|
422
421
|
if (row === null || !hasRefFields)
|
|
423
422
|
return row;
|
|
424
423
|
return enrichRowWithReferences(row, entity, (name) => ctx.registry.getEntity(name), db, {
|
|
425
424
|
user: query.user,
|
|
426
|
-
parentVisibility:
|
|
425
|
+
parentVisibility: parentVisibilityFor(ctx, query.user),
|
|
427
426
|
});
|
|
428
427
|
};
|
|
429
428
|
break;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AggregateKey } from "@cosmicdrift/kumiko-types/aggregate-types";
|
|
2
|
+
import type { ListRuntimeOptions, ParentVisibilityOption } from "@cosmicdrift/kumiko-types/event-store-executor-types";
|
|
2
3
|
import type { EagerLoadEntityResolver, EagerLoadViewer } from "../db/eagerload.ts";
|
|
4
|
+
import type { TenantDb } from "../db/tenant-db.ts";
|
|
3
5
|
import type { HandlerContext, SessionUser } from "./types/index.ts";
|
|
6
|
+
export declare function parentVisibilityFor(ctx: HandlerContext, user: SessionUser): ParentVisibilityOption;
|
|
4
7
|
export declare function entityListRuntime(ctx: HandlerContext, entityName: string, user: SessionUser): {
|
|
5
8
|
readonly runtimeOptions: ListRuntimeOptions;
|
|
6
9
|
readonly eagerLoadViewer: EagerLoadViewer;
|
|
7
10
|
readonly resolveEntity: EagerLoadEntityResolver;
|
|
8
11
|
};
|
|
12
|
+
export declare function referenceGroupLabels(ctx: HandlerContext, user: SessionUser, sourceTableName: string, groupByField: string, keys: readonly AggregateKey[], db: TenantDb): Promise<ReadonlyMap<string, string>>;
|
|
@@ -1,20 +1,119 @@
|
|
|
1
|
+
import { collectEntityDetailAccess, collectEntityReadAccess } from "../api/entity-read-access.js";
|
|
2
|
+
import { resolveTableName } from "../db/entity-table-meta.js";
|
|
3
|
+
import { labelListAggregateGroups } from "../db/list-aggregate-labels.js";
|
|
4
|
+
import { buildEntityTable } from "../db/table-builder.js";
|
|
5
|
+
import { extractTableInfo } from "../db/table-info.js";
|
|
6
|
+
import { hasAccess } from "./access.js";
|
|
7
|
+
import { IMPLICIT_PROJECTION_SUFFIX } from "./registry-state.js";
|
|
8
|
+
// The rules only change with the registry, so they are collected once, not per request.
|
|
9
|
+
const readAccessByRegistry = new WeakMap();
|
|
10
|
+
const detailAccessByRegistry = new WeakMap();
|
|
11
|
+
function detailAccessOf(registry) {
|
|
12
|
+
let rules = detailAccessByRegistry.get(registry);
|
|
13
|
+
if (rules === undefined) {
|
|
14
|
+
rules = collectEntityDetailAccess(registry);
|
|
15
|
+
detailAccessByRegistry.set(registry, rules);
|
|
16
|
+
}
|
|
17
|
+
return rules;
|
|
18
|
+
}
|
|
19
|
+
function readAccessOf(registry) {
|
|
20
|
+
let rules = readAccessByRegistry.get(registry);
|
|
21
|
+
if (rules === undefined) {
|
|
22
|
+
rules = collectEntityReadAccess(registry);
|
|
23
|
+
readAccessByRegistry.set(registry, rules);
|
|
24
|
+
}
|
|
25
|
+
return rules;
|
|
26
|
+
}
|
|
27
|
+
// An entity with a backingTable is read from that table; the implicit projection holds it.
|
|
28
|
+
function readTableOf(ctx, entityName, entity) {
|
|
29
|
+
const implicit = ctx.registry
|
|
30
|
+
.getProjectionsForSource(entityName)
|
|
31
|
+
.find((projection) => projection.name.endsWith(`:projection:${entityName}${IMPLICIT_PROJECTION_SUFFIX}`));
|
|
32
|
+
return implicit?.table ?? buildEntityTable(entityName, entity);
|
|
33
|
+
}
|
|
34
|
+
// The join-row and read gates resolve hosts and file owners against the registry, which only
|
|
35
|
+
// exists at request time; the handler check is the one live signals use too.
|
|
36
|
+
export function parentVisibilityFor(ctx, user) {
|
|
37
|
+
const detailRules = detailAccessOf(ctx.registry);
|
|
38
|
+
return {
|
|
39
|
+
entities: ctx.registry.getAllEntities(),
|
|
40
|
+
canReadEntity: (name) => {
|
|
41
|
+
const rule = detailRules.get(name);
|
|
42
|
+
return rule !== undefined && hasAccess(user, rule);
|
|
43
|
+
},
|
|
44
|
+
resolveTable: (name) => {
|
|
45
|
+
const entity = ctx.registry.getEntity(name);
|
|
46
|
+
if (entity === undefined)
|
|
47
|
+
throw new Error(`parentVisibility: unknown entity "${name}"`);
|
|
48
|
+
return readTableOf(ctx, name, entity);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
1
52
|
// Registry access only exists at request time, so the searchable/sortable-reference
|
|
2
53
|
// caches, the entity resolver and the join-row read gate travel through the
|
|
3
54
|
// runtime-override extension point of the executor verbs.
|
|
4
55
|
export function entityListRuntime(ctx, entityName, user) {
|
|
5
|
-
const parentVisibility =
|
|
56
|
+
const parentVisibility = parentVisibilityFor(ctx, user);
|
|
6
57
|
const resolveEntity = (name) => ctx.registry.getEntity(name);
|
|
58
|
+
const resolveTable = (name) => {
|
|
59
|
+
const entity = ctx.registry.getEntity(name);
|
|
60
|
+
if (entity === undefined)
|
|
61
|
+
throw new Error(`entityListRuntime: unknown entity "${name}"`);
|
|
62
|
+
return readTableOf(ctx, name, entity);
|
|
63
|
+
};
|
|
64
|
+
const readRules = readAccessOf(ctx.registry);
|
|
7
65
|
return {
|
|
8
66
|
runtimeOptions: {
|
|
9
67
|
...(ctx.services.searchAdapter !== undefined && {
|
|
10
68
|
searchAdapter: ctx.services.searchAdapter,
|
|
11
69
|
}),
|
|
12
70
|
...(ctx.includeDeleted === true && { includeDeleted: true }),
|
|
13
|
-
referenceSearch: {
|
|
14
|
-
|
|
71
|
+
referenceSearch: {
|
|
72
|
+
fields: ctx.registry.getSearchableReferences(entityName),
|
|
73
|
+
resolveEntity,
|
|
74
|
+
resolveTable,
|
|
75
|
+
},
|
|
76
|
+
referenceSort: {
|
|
77
|
+
fields: ctx.registry.getSortableReferences(entityName),
|
|
78
|
+
resolveEntity,
|
|
79
|
+
resolveTable,
|
|
80
|
+
},
|
|
15
81
|
parentVisibility,
|
|
82
|
+
derivedSql: {
|
|
83
|
+
resolveEntity,
|
|
84
|
+
resolveTable,
|
|
85
|
+
canReadEntity: (name) => (readRules.get(name) ?? []).some((rule) => hasAccess(user, rule)),
|
|
86
|
+
today: ctx.tz.today(ctx.tz.tenant).toString(),
|
|
87
|
+
now: ctx.tz.now().toString(),
|
|
88
|
+
},
|
|
16
89
|
},
|
|
17
|
-
eagerLoadViewer: { user, parentVisibility },
|
|
90
|
+
eagerLoadViewer: { user, parentVisibility, resolveTable },
|
|
18
91
|
resolveEntity,
|
|
19
92
|
};
|
|
20
93
|
}
|
|
94
|
+
// An entity with a backingTable reads and writes that table, whose name the entity's own
|
|
95
|
+
// `table` does not say; the implicit projection holds the table the executor really uses.
|
|
96
|
+
function physicalReadTableName(ctx, entityName, entity) {
|
|
97
|
+
const implicit = ctx.registry
|
|
98
|
+
.getProjectionsForSource(entityName)
|
|
99
|
+
.find((projection) => projection.name.endsWith(`:projection:${entityName}${IMPLICIT_PROJECTION_SUFFIX}`));
|
|
100
|
+
return implicit !== undefined
|
|
101
|
+
? extractTableInfo(implicit.table).name
|
|
102
|
+
: resolveTableName(entityName, entity, undefined);
|
|
103
|
+
}
|
|
104
|
+
// Resolved through the viewer's read access, like list-aggregate labels; non-entity sources yield none.
|
|
105
|
+
export async function referenceGroupLabels(ctx, user, sourceTableName, groupByField, keys, db) {
|
|
106
|
+
const labels = new Map();
|
|
107
|
+
const owner = [...ctx.registry.getAllEntities()].find(([entityName, entity]) => physicalReadTableName(ctx, entityName, entity) === sourceTableName);
|
|
108
|
+
if (owner === undefined || keys.length === 0)
|
|
109
|
+
return labels;
|
|
110
|
+
const [entityName, entity] = owner;
|
|
111
|
+
const { eagerLoadViewer, resolveEntity } = entityListRuntime(ctx, entityName, user);
|
|
112
|
+
const labeled = await labelListAggregateGroups({ groups: keys.map((key) => ({ key, count: 0, measures: [] })) }, groupByField, { entity, resolveEntity, viewer: eagerLoadViewer }, db);
|
|
113
|
+
for (const group of labeled.groups) {
|
|
114
|
+
if (group.key !== null && group.label != null && group.label !== "") {
|
|
115
|
+
labels.set(String(group.key), group.label);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return labels;
|
|
119
|
+
}
|
|
@@ -17,11 +17,9 @@ export declare function createTextField<R extends true | false = false>(override
|
|
|
17
17
|
* (for example, `1_000_000 = 1 MB`).
|
|
18
18
|
*/
|
|
19
19
|
/**
|
|
20
|
-
* Read-time computed field for `EntityDefinition.derivedFields` (NOT `fields`).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* apply and there's no client-side sort path (see DerivedFieldDef). `derive`
|
|
24
|
-
* must take its clock from `ctx.asOf`, never `Temporal.Now`/`Date`.
|
|
20
|
+
* Read-time computed field for `EntityDefinition.derivedFields` (NOT `fields`), never persisted.
|
|
21
|
+
* `derive` (display only, clock from `ctx.asOf`) or `sql` (computed in the list SELECT, optionally
|
|
22
|
+
* `sortable`) — see DerivedFieldDef.
|
|
25
23
|
*/
|
|
26
24
|
export declare function createDerivedField(spec: DerivedFieldDef): DerivedFieldDef;
|
|
27
25
|
export declare function createLongTextField<R extends true | false = false>(overrides: Partial<Omit<LongTextFieldDef, "type" | "required" | keyof ResolvedPiiFlags>> & PersonalAnnotationsLongText & {
|
|
@@ -194,6 +192,8 @@ export declare function createEntity<const F, const T extends EntityTenancy = "t
|
|
|
194
192
|
readonly access?: EntityDefinition["access"];
|
|
195
193
|
/** Join-row carrier reference — see EntityDefinition.parentRef. */
|
|
196
194
|
readonly parentRef?: EntityDefinition["parentRef"];
|
|
195
|
+
/** See EntityDefinition.readGate. */
|
|
196
|
+
readonly readGate?: EntityDefinition["readGate"];
|
|
197
197
|
readonly retention?: RetentionDef;
|
|
198
198
|
readonly derivedFields?: EntityDefinition["derivedFields"];
|
|
199
199
|
readonly tenancy?: T;
|
package/dist/engine/factories.js
CHANGED
|
@@ -137,11 +137,9 @@ export function createTextField(overrides) {
|
|
|
137
137
|
* (for example, `1_000_000 = 1 MB`).
|
|
138
138
|
*/
|
|
139
139
|
/**
|
|
140
|
-
* Read-time computed field for `EntityDefinition.derivedFields` (NOT `fields`).
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* apply and there's no client-side sort path (see DerivedFieldDef). `derive`
|
|
144
|
-
* must take its clock from `ctx.asOf`, never `Temporal.Now`/`Date`.
|
|
140
|
+
* Read-time computed field for `EntityDefinition.derivedFields` (NOT `fields`), never persisted.
|
|
141
|
+
* `derive` (display only, clock from `ctx.asOf`) or `sql` (computed in the list SELECT, optionally
|
|
142
|
+
* `sortable`) — see DerivedFieldDef.
|
|
145
143
|
*/
|
|
146
144
|
export function createDerivedField(spec) {
|
|
147
145
|
return { ...spec };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EntityDefinition } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
+
import { type OwnershipMap } from "./ownership.ts";
|
|
2
3
|
import type { SessionUser } from "./types/index.ts";
|
|
3
4
|
export declare function maskWriteOnlyFields<T extends Readonly<Record<string, unknown>>>(entity: EntityDefinition, row: T): T;
|
|
4
5
|
export declare function dropEmptyWriteOnlyValues<T extends Readonly<Record<string, unknown>>>(entity: EntityDefinition, values: T): T;
|
|
@@ -6,3 +7,4 @@ export declare function userMayReadFieldInBulk(entity: EntityDefinition, fieldNa
|
|
|
6
7
|
export declare function filterReadFields(entity: EntityDefinition, data: Readonly<Record<string, unknown>>, user: SessionUser): Record<string, unknown>;
|
|
7
8
|
export declare function checkWriteFieldRoles(entity: EntityDefinition, changes: Readonly<Record<string, unknown>>, user: SessionUser): string | null;
|
|
8
9
|
export declare function checkWriteFieldOwnership(entity: EntityDefinition, submittedChanges: Readonly<Record<string, unknown>>, user: SessionUser, oldRow?: Readonly<Record<string, unknown>>, rowContext?: Readonly<Record<string, unknown>>): string | null;
|
|
10
|
+
export declare function userHasWriteRole(user: Pick<SessionUser, "roles">, accessMap: OwnershipMap): boolean;
|