@protobase/schema 0.50.1 → 0.51.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/dist/access/record-probe.js +32 -0
- package/dist/access/resolve.d.ts +4 -1
- package/dist/access/resolve.js +12 -2
- package/dist/access/types.d.ts +3 -2
- package/dist/humanize.d.ts +7 -0
- package/dist/humanize.js +12 -0
- package/dist/internal.d.ts +3 -1
- package/dist/internal.js +3 -1
- package/dist/record-title.d.ts +9 -0
- package/dist/record-title.js +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/access/record-probe.ts
|
|
2
|
+
/** Thrown when a rule called without a record reads the record; a `TypeError`, as a missing record has always thrown. */
|
|
3
|
+
var RecordNeeded = class extends TypeError {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("The rule reads the record");
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A stand-in for the record and the input while a rule is called without them: any read of it throws `RecordNeeded`
|
|
10
|
+
* until `disarm`, so `record?.x`, a default parameter and `args[1].x` all show that the rule needs the record. Once
|
|
11
|
+
* disarmed it acts as an empty object, so a rule that kept it trips nothing later.
|
|
12
|
+
*/
|
|
13
|
+
const recordProbe = () => {
|
|
14
|
+
let armed = true;
|
|
15
|
+
const trap = (fallback) => () => {
|
|
16
|
+
if (armed) throw new RecordNeeded();
|
|
17
|
+
return fallback();
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
value: new Proxy({}, {
|
|
21
|
+
get: trap(() => void 0),
|
|
22
|
+
has: trap(() => false),
|
|
23
|
+
ownKeys: trap(() => []),
|
|
24
|
+
getOwnPropertyDescriptor: trap(() => void 0)
|
|
25
|
+
}),
|
|
26
|
+
disarm: () => {
|
|
27
|
+
armed = false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { RecordNeeded, recordProbe };
|
package/dist/access/resolve.d.ts
CHANGED
|
@@ -44,7 +44,10 @@ export declare const accessWarnings: (options: ResolveOptions) => string[];
|
|
|
44
44
|
export type RowFilterOperation = 'read' | 'create' | 'update' | 'delete';
|
|
45
45
|
/** What one request may do with one resource, as `resolveAccess` works it out. */
|
|
46
46
|
export type ResolvedAccess = {
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Whether each operation is allowed at all; one in `recordChecks` or `rowFilter` only for some records. `list` is
|
|
49
|
+
* never record-checked: it is denied when the read or list rule needs the record.
|
|
50
|
+
*/
|
|
48
51
|
operations: Record<AccessAction, boolean>;
|
|
49
52
|
/** Operations whose rule needs the record (or input); check them with `checkRecord`. */
|
|
50
53
|
recordChecks: AccessAction[];
|
package/dist/access/resolve.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { checkFilter } from "../filter/check.js";
|
|
2
2
|
import { evaluateFilter } from "../filter/evaluate.js";
|
|
3
3
|
import { andResults, normalize } from "./combine.js";
|
|
4
|
+
import { recordProbe } from "./record-probe.js";
|
|
4
5
|
import "./role-entry.js";
|
|
5
6
|
//#region src/access/resolve.ts
|
|
6
7
|
/** Startup warnings for risky option combinations; the caller decides how to report them. */
|
|
@@ -39,11 +40,17 @@ const checked = (model, expr, operation) => {
|
|
|
39
40
|
return result.filter;
|
|
40
41
|
};
|
|
41
42
|
const decide = async (rule, ctx) => {
|
|
43
|
+
const probe = recordProbe();
|
|
42
44
|
try {
|
|
43
|
-
return normalize(await rule(
|
|
45
|
+
return normalize(await rule({
|
|
46
|
+
...ctx,
|
|
47
|
+
record: probe.value
|
|
48
|
+
}, probe.value, probe.value));
|
|
44
49
|
} catch (error) {
|
|
45
50
|
if (error instanceof TypeError) return "record";
|
|
46
51
|
throw error;
|
|
52
|
+
} finally {
|
|
53
|
+
probe.disarm();
|
|
47
54
|
}
|
|
48
55
|
};
|
|
49
56
|
const verdict = async (rule, ctx, fallback, what) => {
|
|
@@ -66,7 +73,6 @@ const resolveAccess = async (source, ctx, options = {}) => {
|
|
|
66
73
|
for (const operation of operations) {
|
|
67
74
|
if (operation === "list" && !source.accessRules.list) {
|
|
68
75
|
allowed.list = allowed.read;
|
|
69
|
-
if (recordChecks.includes("read")) recordChecks.push("list");
|
|
70
76
|
continue;
|
|
71
77
|
}
|
|
72
78
|
const context = {
|
|
@@ -88,6 +94,10 @@ const resolveAccess = async (source, ctx, options = {}) => {
|
|
|
88
94
|
if (typeof combined === "object") filters[operation] = combined;
|
|
89
95
|
}
|
|
90
96
|
}
|
|
97
|
+
if (recordChecks.includes("read") || recordChecks.includes("list")) {
|
|
98
|
+
allowed.list = false;
|
|
99
|
+
if (recordChecks.includes("list")) recordChecks.splice(recordChecks.indexOf("list"), 1);
|
|
100
|
+
}
|
|
91
101
|
const fieldRules = source.fieldAccess();
|
|
92
102
|
const fieldContext = (operation) => ({
|
|
93
103
|
...ctx,
|
package/dist/access/types.d.ts
CHANGED
|
@@ -34,8 +34,9 @@ export type AccessContext = {
|
|
|
34
34
|
export type AccessResult = boolean | string | FilterExpr;
|
|
35
35
|
/**
|
|
36
36
|
* A rule at the resource level. Without a record it decides the operation and may return a row
|
|
37
|
-
* filter; with `(ctx, record, input)` it is a record-level check.
|
|
38
|
-
*
|
|
37
|
+
* filter; with `(ctx, record, input)` it is a record-level check. A rule that reads the record or
|
|
38
|
+
* input when called without them, or throws a `TypeError`, marks the operation as record-checked;
|
|
39
|
+
* a list is then refused, since it does not run the rule on each row.
|
|
39
40
|
*/
|
|
40
41
|
export type AccessFn<R = any> = (ctx: AccessContext, record?: R, input?: Partial<R>) => AccessResult | Promise<AccessResult>;
|
|
41
42
|
/** A field rule is role-based: it never sees a record, so readable columns are fixed per request. */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/humanize.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A field's or resource's name in words, where its view gives no label: `companyId` and `company_id` become "Company",
|
|
4
|
+
* `discountPercent` and `discount_percent` become "Discount percent".
|
|
5
|
+
*/
|
|
6
|
+
export declare const humanize: (name: string) => string;
|
|
7
|
+
//#endregion
|
package/dist/humanize.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/humanize.ts
|
|
2
|
+
const capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
3
|
+
/**
|
|
4
|
+
* A field's or resource's name in words, where its view gives no label: `companyId` and `company_id` become "Company",
|
|
5
|
+
* `discountPercent` and `discount_percent` become "Discount percent".
|
|
6
|
+
*/
|
|
7
|
+
const humanize = (name) => {
|
|
8
|
+
const words = name.replace(/(Id|Code|_id|_code)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/_+/g, " ").trim().toLowerCase();
|
|
9
|
+
return capitalise(words || name);
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { humanize };
|
package/dist/internal.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { readEventStream } from "./assistant/event-stream.js";
|
|
|
25
25
|
import { layoutItems } from "./layout-items.js";
|
|
26
26
|
import { viewOfList } from "./named-list.js";
|
|
27
27
|
import { Locale, isLocale, locales, pickLocale } from "./locale.js";
|
|
28
|
+
import { humanize } from "./humanize.js";
|
|
29
|
+
import { labelFieldOf, recordTitle, recordTitleField } from "./record-title.js";
|
|
28
30
|
import { apiOriginMeta } from "./api-origin-meta.js";
|
|
29
31
|
import { MobileReleaseAnswer, mobileReleaseBasePath, mobileReleaseParams, mobileReleasePath, mobileReleaseQuery, mobileReleasesPath, releaseManifestFile } from "./mobile-release-address.js";
|
|
30
32
|
import { authBasePath } from "./auth-base-path.js";
|
|
@@ -32,4 +34,4 @@ import { capacitorOrigin } from "./capacitor-origins.js";
|
|
|
32
34
|
import { pushRecord, serviceWorkerFile } from "./push-record.js";
|
|
33
35
|
import { deviceIdParam, devicesPath } from "./devices-path.js";
|
|
34
36
|
import { historyIdParam, historyIdPattern, historyPath } from "./history-path.js";
|
|
35
|
-
export { type AddressCheck, type FormulaNode, type Locale, type MobileReleaseAnswer, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionConditionError, actionInputErrors, addressErrors, addressPlaceholders, apiOriginMeta, appHrefError, applyAssistantEvent, authBasePath, boardErrors, bottomBarFor, capacitorOrigin, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, datesErrors, decodeKey, defaultRecentLimit, deviceIdParam, devicesPath, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, historyIdParam, historyIdPattern, historyPath, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, isLocale, layoutItems, leavesApp, linkActionError, locales, markFieldRefs, matchedSearchField, matchesSearch, mobileReleaseBasePath, mobileReleaseParams, mobileReleasePath, mobileReleaseQuery, mobileReleasesPath, navRecentErrors, parseCapability, parseFormula, parseSize, parseTable, pickLocale, pluginProblem, promptError, promptFields, pruneByShape, pushRecord, readEventStream, redactSecrets, releaseManifestFile, renderPrompt, searchTerms, serviceWorkerFile, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, updateSetErrors, updateValues, viewOfList, webhookResources };
|
|
37
|
+
export { type AddressCheck, type FormulaNode, type Locale, type MobileReleaseAnswer, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionConditionError, actionInputErrors, addressErrors, addressPlaceholders, apiOriginMeta, appHrefError, applyAssistantEvent, authBasePath, boardErrors, bottomBarFor, capacitorOrigin, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, datesErrors, decodeKey, defaultRecentLimit, deviceIdParam, devicesPath, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, historyIdParam, historyIdPattern, historyPath, hrefField, hrefPlaceholders, humanize, inRange, isFieldRef, isGroupChart, isLocale, labelFieldOf, layoutItems, leavesApp, linkActionError, locales, markFieldRefs, matchedSearchField, matchesSearch, mobileReleaseBasePath, mobileReleaseParams, mobileReleasePath, mobileReleaseQuery, mobileReleasesPath, navRecentErrors, parseCapability, parseFormula, parseSize, parseTable, pickLocale, pluginProblem, promptError, promptFields, pruneByShape, pushRecord, readEventStream, recordTitle, recordTitleField, redactSecrets, releaseManifestFile, renderPrompt, searchTerms, serviceWorkerFile, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, updateSetErrors, updateValues, viewOfList, webhookResources };
|
package/dist/internal.js
CHANGED
|
@@ -25,6 +25,8 @@ import { addressErrors } from "./address-errors.js";
|
|
|
25
25
|
import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
|
|
26
26
|
import { readEventStream } from "./assistant/event-stream.js";
|
|
27
27
|
import { isLocale, locales, pickLocale } from "./locale.js";
|
|
28
|
+
import { humanize } from "./humanize.js";
|
|
29
|
+
import { labelFieldOf, recordTitle, recordTitleField } from "./record-title.js";
|
|
28
30
|
import { apiOriginMeta } from "./api-origin-meta.js";
|
|
29
31
|
import { mobileReleaseBasePath, mobileReleaseParams, mobileReleasePath, mobileReleaseQuery, mobileReleasesPath, releaseManifestFile } from "./mobile-release-address.js";
|
|
30
32
|
import { authBasePath } from "./auth-base-path.js";
|
|
@@ -32,4 +34,4 @@ import { capacitorOrigin } from "./capacitor-origins.js";
|
|
|
32
34
|
import { pushRecord, serviceWorkerFile } from "./push-record.js";
|
|
33
35
|
import { deviceIdParam, devicesPath } from "./devices-path.js";
|
|
34
36
|
import { historyIdParam, historyIdPattern, historyPath } from "./history-path.js";
|
|
35
|
-
export { actionConditionError, actionInputErrors, addressErrors, addressPlaceholders, apiOriginMeta, appHrefError, applyAssistantEvent, authBasePath, boardErrors, bottomBarFor, capacitorOrigin, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, datesErrors, decodeKey, defaultRecentLimit, deviceIdParam, devicesPath, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, historyIdParam, historyIdPattern, historyPath, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, isLocale, layoutItems, leavesApp, linkActionError, locales, markFieldRefs, matchedSearchField, matchesSearch, mobileReleaseBasePath, mobileReleaseParams, mobileReleasePath, mobileReleaseQuery, mobileReleasesPath, navRecentErrors, parseCapability, parseFormula, parseSize, parseTable, pickLocale, pluginProblem, promptError, promptFields, pruneByShape, pushRecord, readEventStream, redactSecrets, releaseManifestFile, renderPrompt, searchTerms, serviceWorkerFile, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, updateSetErrors, updateValues, viewOfList, webhookResources };
|
|
37
|
+
export { actionConditionError, actionInputErrors, addressErrors, addressPlaceholders, apiOriginMeta, appHrefError, applyAssistantEvent, authBasePath, boardErrors, bottomBarFor, capacitorOrigin, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, datesErrors, decodeKey, defaultRecentLimit, deviceIdParam, devicesPath, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, historyIdParam, historyIdPattern, historyPath, hrefField, hrefPlaceholders, humanize, inRange, isFieldRef, isGroupChart, isLocale, labelFieldOf, layoutItems, leavesApp, linkActionError, locales, markFieldRefs, matchedSearchField, matchesSearch, mobileReleaseBasePath, mobileReleaseParams, mobileReleasePath, mobileReleaseQuery, mobileReleasesPath, navRecentErrors, parseCapability, parseFormula, parseSize, parseTable, pickLocale, pluginProblem, promptError, promptFields, pruneByShape, pushRecord, readEventStream, recordTitle, recordTitleField, redactSecrets, releaseManifestFile, renderPrompt, searchTerms, serviceWorkerFile, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, updateSetErrors, updateValues, viewOfList, webhookResources };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ResourceModel, ViewModel } from "./model.js";
|
|
2
|
+
//#region src/record-title.d.ts
|
|
3
|
+
/** The field that names a record when another record points at it. */
|
|
4
|
+
export declare const labelFieldOf: (model: ResourceModel, view?: ViewModel) => string;
|
|
5
|
+
/** What names a record: the view's `title`, else the first text column of its list, else `labelFieldOf`. */
|
|
6
|
+
export declare const recordTitleField: (model: ResourceModel, view: ViewModel | undefined) => string;
|
|
7
|
+
/** A record's name as shown: its title field's value, an enum's by its label, else `fallback` when the value is missing. */
|
|
8
|
+
export declare const recordTitle: (model: ResourceModel, view: ViewModel | undefined, record: Record<string, unknown>, fallback: string) => string;
|
|
9
|
+
//#endregion
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { enumLabel } from "./enum-label.js";
|
|
2
|
+
//#region src/record-title.ts
|
|
3
|
+
const labelCandidates = [
|
|
4
|
+
"name",
|
|
5
|
+
"title",
|
|
6
|
+
"number",
|
|
7
|
+
"sku",
|
|
8
|
+
"email",
|
|
9
|
+
"code",
|
|
10
|
+
"description"
|
|
11
|
+
];
|
|
12
|
+
const namesRecords = (field) => field?.type === "text" && !field.sensitive;
|
|
13
|
+
/** The field that names a record when another record points at it. */
|
|
14
|
+
const labelFieldOf = (model, view) => view?.title ?? labelCandidates.find((name) => namesRecords(model.fields[name])) ?? Object.values(model.fields).find(namesRecords)?.name ?? model.primaryKey[0];
|
|
15
|
+
/** What names a record: the view's `title`, else the first text column of its list, else `labelFieldOf`. */
|
|
16
|
+
const recordTitleField = (model, view) => view?.title ?? view?.list?.columns.find((name) => model.fields[name]?.type === "text") ?? labelFieldOf(model);
|
|
17
|
+
/** A record's name as shown: its title field's value, an enum's by its label, else `fallback` when the value is missing. */
|
|
18
|
+
const recordTitle = (model, view, record, fallback) => {
|
|
19
|
+
const name = recordTitleField(model, view);
|
|
20
|
+
const value = record[name];
|
|
21
|
+
if (value === null || value === void 0) return fallback;
|
|
22
|
+
return model.fields[name]?.type === "enum" ? enumLabel(String(value), view?.fields[name]?.valueLabels) : String(value);
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { labelFieldOf, recordTitle, recordTitleField };
|