@protobase/schema 0.0.0-stage → 0.1.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.
Files changed (91) hide show
  1. package/LICENSE +202 -0
  2. package/dist/access/capability.d.ts +10 -0
  3. package/dist/access/capability.js +30 -0
  4. package/dist/access/combine.js +39 -0
  5. package/dist/access/index.d.ts +8 -0
  6. package/dist/access/pick-view.d.ts +8 -0
  7. package/dist/access/pick-view.js +11 -0
  8. package/dist/access/resolve.d.ts +61 -0
  9. package/dist/access/resolve.js +150 -0
  10. package/dist/access/restrict.d.ts +16 -0
  11. package/dist/access/restrict.js +37 -0
  12. package/dist/access/roles.d.ts +30 -0
  13. package/dist/access/roles.js +55 -0
  14. package/dist/access/rule.d.ts +11 -0
  15. package/dist/access/rule.js +9 -0
  16. package/dist/access/types.d.ts +37 -0
  17. package/dist/enum-label.d.ts +4 -0
  18. package/dist/enum-label.js +5 -0
  19. package/dist/field.d.ts +69 -0
  20. package/dist/field.js +85 -0
  21. package/dist/fields/bigint.js +12 -0
  22. package/dist/fields/boolean.js +9 -0
  23. package/dist/fields/country.js +9 -0
  24. package/dist/fields/currency.js +9 -0
  25. package/dist/fields/date.js +9 -0
  26. package/dist/fields/decimal.js +22 -0
  27. package/dist/fields/enum.js +12 -0
  28. package/dist/fields/integer.js +14 -0
  29. package/dist/fields/json.js +9 -0
  30. package/dist/fields/relation.js +9 -0
  31. package/dist/fields/text.js +19 -0
  32. package/dist/fields/timestamp.js +9 -0
  33. package/dist/fields/uuid.js +9 -0
  34. package/dist/fields.d.ts +23 -0
  35. package/dist/fields.js +49 -0
  36. package/dist/filter/check-field.js +25 -0
  37. package/dist/filter/check-model.js +123 -0
  38. package/dist/filter/check-value.js +64 -0
  39. package/dist/filter/check.d.ts +16 -0
  40. package/dist/filter/check.js +22 -0
  41. package/dist/filter/datetime.js +33 -0
  42. package/dist/filter/duration.js +16 -0
  43. package/dist/filter/errors.js +29 -0
  44. package/dist/filter/evaluate.d.ts +12 -0
  45. package/dist/filter/evaluate.js +33 -0
  46. package/dist/filter/index.d.ts +7 -0
  47. package/dist/filter/lift.js +107 -0
  48. package/dist/filter/lower.js +170 -0
  49. package/dist/filter/operators.js +26 -0
  50. package/dist/filter/order-by.d.ts +23 -0
  51. package/dist/filter/order-by.js +51 -0
  52. package/dist/filter/parse.d.ts +20 -0
  53. package/dist/filter/parse.js +28 -0
  54. package/dist/filter/print.d.ts +5 -0
  55. package/dist/filter/print.js +7 -0
  56. package/dist/filter/profile.js +69 -0
  57. package/dist/filter/where.d.ts +58 -0
  58. package/dist/filter/where.js +121 -0
  59. package/dist/index.d.ts +28 -0
  60. package/dist/index.js +21 -0
  61. package/dist/keys.d.ts +12 -0
  62. package/dist/keys.js +57 -0
  63. package/dist/layout.d.ts +12 -0
  64. package/dist/layout.js +21 -0
  65. package/dist/model.d.ts +333 -0
  66. package/dist/naming.js +4 -0
  67. package/dist/nav-recent.d.ts +8 -0
  68. package/dist/nav-recent.js +37 -0
  69. package/dist/refs.d.ts +11 -0
  70. package/dist/refs.js +8 -0
  71. package/dist/relation-field.d.ts +6 -0
  72. package/dist/relation-field.js +12 -0
  73. package/dist/resource-model.d.ts +15 -0
  74. package/dist/resource-model.js +71 -0
  75. package/dist/resource-types.d.ts +21 -0
  76. package/dist/resource.d.ts +47 -0
  77. package/dist/resource.js +97 -0
  78. package/dist/search-match.d.ts +29 -0
  79. package/dist/search-match.js +50 -0
  80. package/dist/user-menu.d.ts +25 -0
  81. package/dist/user-menu.js +32 -0
  82. package/dist/view-actions.d.ts +24 -0
  83. package/dist/view-actions.js +35 -0
  84. package/dist/view-field.d.ts +16 -0
  85. package/dist/view-field.js +39 -0
  86. package/dist/view-widgets.d.ts +16 -0
  87. package/dist/view-widgets.js +24 -0
  88. package/dist/view.d.ts +69 -0
  89. package/dist/view.js +109 -0
  90. package/package.json +31 -3
  91. package/README.md +0 -3
@@ -0,0 +1,23 @@
1
+ import { FilterError, OrderByItem, ResourceModel, SortSpec } from "../model.js";
2
+ //#region src/filter/order-by.d.ts
3
+ export type OrderByParse = {
4
+ ok: true;
5
+ items: OrderByItem[];
6
+ errors: [];
7
+ } | {
8
+ ok: false;
9
+ items: OrderByItem[];
10
+ errors: FilterError[];
11
+ };
12
+ export type OrderByCheck = {
13
+ ok: true;
14
+ sort: SortSpec;
15
+ } | {
16
+ ok: false;
17
+ errors: FilterError[];
18
+ };
19
+ export declare const parseOrderBy: (source: string) => OrderByParse;
20
+ export declare const printOrderBy: (items: OrderByItem[]) => string;
21
+ /** Validates against sortable fields; traversal is not supported yet. */
22
+ export declare const checkOrderBy: (model: ResourceModel, items: OrderByItem[]) => OrderByCheck;
23
+ //#endregion
@@ -0,0 +1,51 @@
1
+ import { filterError } from "./errors.js";
2
+ import { schemaOf } from "./profile.js";
3
+ import { checkOrderBy, parseOrderBy, printOrderBy } from "aip-parsers/order-by";
4
+ //#region src/filter/order-by.ts
5
+ const generic = (items) => items.map((item) => ({
6
+ path: item.path.path,
7
+ direction: item.direction,
8
+ span: item.span
9
+ }));
10
+ const lowercaseDirections = (source) => source.replace(/([^\s,])(\s+)(asc|desc)(?=\s|,|$)/gi, (_, before, space, word) => `${before}${space}${word.toLowerCase()}`);
11
+ const parseOrderBy$1 = (source) => {
12
+ const parsed = parseOrderBy(lowercaseDirections(source));
13
+ const items = parsed.items.map((item) => ({
14
+ path: {
15
+ kind: "field",
16
+ path: item.path,
17
+ span: item.span
18
+ },
19
+ direction: item.direction,
20
+ span: item.span
21
+ }));
22
+ return parsed.ok ? {
23
+ ok: true,
24
+ items,
25
+ errors: []
26
+ } : {
27
+ ok: false,
28
+ items,
29
+ errors: parsed.errors
30
+ };
31
+ };
32
+ const printOrderBy$1 = (items) => printOrderBy(generic(items));
33
+ /** Validates against sortable fields; traversal is not supported yet. */
34
+ const checkOrderBy$1 = (model, items) => {
35
+ const traversal = items.filter((item) => item.path.path.length > 1).map((item) => filterError("unsupported-traversal", `Traversal into "${item.path.path.join(".")}" is not supported yet`, "Sort on fields of this resource only", item.path.span));
36
+ if (traversal.length > 0) return {
37
+ ok: false,
38
+ errors: traversal
39
+ };
40
+ const checked = checkOrderBy(generic(items), schemaOf(model));
41
+ if (!checked.ok) return {
42
+ ok: false,
43
+ errors: checked.errors
44
+ };
45
+ return {
46
+ ok: true,
47
+ sort: checked.items.map((item) => [item.path[0], item.direction])
48
+ };
49
+ };
50
+ //#endregion
51
+ export { checkOrderBy$1 as checkOrderBy, parseOrderBy$1 as parseOrderBy, printOrderBy$1 as printOrderBy };
@@ -0,0 +1,20 @@
1
+ import { FilterError, FilterExpr } from "../model.js";
2
+ import { ParseOptions } from "aip-parsers/filter";
3
+ //#region src/filter/parse.d.ts
4
+ export type FilterParse = {
5
+ ok: true;
6
+ ast: FilterExpr | undefined;
7
+ errors: [];
8
+ } | {
9
+ ok: false;
10
+ ast: FilterExpr | undefined;
11
+ errors: FilterError[];
12
+ };
13
+ export type FilterLimits = Pick<ParseOptions, 'maxLength' | 'maxDepth'>;
14
+ /**
15
+ * Parses with the library, then lowers to Protobase's filter shape. Collects all errors.
16
+ * `maxLength` defaults to 10,000 characters and `maxDepth` to 64.
17
+ */
18
+ declare const parseFilter$1: (source: string, limits?: FilterLimits) => FilterParse;
19
+ //#endregion
20
+ export { parseFilter$1 as parseFilter };
@@ -0,0 +1,28 @@
1
+ import { parseOptions } from "./profile.js";
2
+ import { lower } from "./lower.js";
3
+ import { parseFilter } from "aip-parsers/filter";
4
+ //#region src/filter/parse.ts
5
+ /**
6
+ * Parses with the library, then lowers to Protobase's filter shape. Collects all errors.
7
+ * `maxLength` defaults to 10,000 characters and `maxDepth` to 64.
8
+ */
9
+ const parseFilter$1 = (source, limits = {}) => {
10
+ const parsed = parseFilter(source, {
11
+ ...parseOptions,
12
+ ...limits
13
+ });
14
+ const lowering = { errors: [] };
15
+ const ast = parsed.ast && lower(lowering, parsed.ast);
16
+ const errors = [...parsed.errors, ...lowering.errors].sort((a, b) => a.span.start - b.span.start);
17
+ return errors.length === 0 ? {
18
+ ok: true,
19
+ ast,
20
+ errors: []
21
+ } : {
22
+ ok: false,
23
+ ast,
24
+ errors
25
+ };
26
+ };
27
+ //#endregion
28
+ export { parseFilter$1 as parseFilter };
@@ -0,0 +1,5 @@
1
+ import { FilterExpr } from "../model.js";
2
+ //#region src/filter/print.d.ts
3
+ /** Canonical text, readable by `parseFilter`; OR groups inside AND are always parenthesised. */
4
+ export declare const printFilter: (ast: FilterExpr) => string;
5
+ //#endregion
@@ -0,0 +1,7 @@
1
+ import { lift } from "./lift.js";
2
+ import { printFilter } from "aip-parsers/filter";
3
+ //#region src/filter/print.ts
4
+ /** Canonical text, readable by `parseFilter`; OR groups inside AND are always parenthesised. */
5
+ const printFilter$1 = (ast) => printFilter(lift(ast));
6
+ //#endregion
7
+ export { printFilter$1 as printFilter };
@@ -0,0 +1,69 @@
1
+ //#region src/filter/profile.ts
2
+ /** Protobase's filter functions, declared through the library's registry. */
3
+ const functions = {
4
+ in: {
5
+ params: [{
6
+ name: "field",
7
+ type: "field"
8
+ }],
9
+ rest: {
10
+ name: "value",
11
+ type: "any"
12
+ }
13
+ },
14
+ search: { params: [{
15
+ name: "text",
16
+ type: "string"
17
+ }] },
18
+ similar: { params: [{
19
+ name: "field",
20
+ type: "field",
21
+ of: ["string"]
22
+ }, {
23
+ name: "text",
24
+ type: "string"
25
+ }] },
26
+ regex: { params: [{
27
+ name: "field",
28
+ type: "field",
29
+ of: ["string"]
30
+ }, {
31
+ name: "pattern",
32
+ type: "string"
33
+ }] },
34
+ isNull: { params: [{
35
+ name: "field",
36
+ type: "field"
37
+ }] },
38
+ now: {
39
+ params: [],
40
+ returns: "timestamp"
41
+ }
42
+ };
43
+ const parseOptions = { extensions: {
44
+ arithmetic: "now-offset",
45
+ durationUnits: true
46
+ } };
47
+ const libraryType = (field) => {
48
+ switch (field.type) {
49
+ case "text": return "string";
50
+ case "integer":
51
+ case "bigint": return "integer";
52
+ case "boolean":
53
+ case "timestamp":
54
+ case "enum": return field.type;
55
+ default: return "any";
56
+ }
57
+ };
58
+ const schemaOf = (model) => ({
59
+ fields: Object.fromEntries(Object.values(model.fields).map((field) => [field.name, {
60
+ type: libraryType(field),
61
+ filterable: field.filterable,
62
+ sortable: field.sortable,
63
+ aliases: field.aliases,
64
+ ...field.enumValues && { values: field.enumValues }
65
+ }])),
66
+ functions
67
+ });
68
+ //#endregion
69
+ export { functions, libraryType, parseOptions, schemaOf };
@@ -0,0 +1,58 @@
1
+ import { FilterExpr, FilterValue } from "../model.js";
2
+ import { Field } from "../field.js";
3
+ import { RecordOf } from "../resource-types.js";
4
+ //#region src/filter/where.d.ts
5
+ type Scalar = string | number | boolean;
6
+ type Source = {
7
+ readonly $fields: unknown;
8
+ };
9
+ type Fields<R extends Source> = keyof RecordOf<R['$fields']> & string;
10
+ type Value<R extends Source, K extends Fields<R>> = Extract<NonNullable<RecordOf<R['$fields']>[K]>, Scalar> | FilterValue;
11
+ type Anything = {
12
+ readonly $fields: Record<string, Field<any>>;
13
+ };
14
+ export declare const createWhere: <R extends Source = Anything>() => {
15
+ eq: <K extends Fields<R>>(name: K, value: Value<R, K>) => FilterExpr;
16
+ ne: <K extends Fields<R>>(name: K, value: Value<R, K>) => FilterExpr;
17
+ lt: <K extends Fields<R>>(name: K, value: Value<R, K>) => FilterExpr;
18
+ lte: <K extends Fields<R>>(name: K, value: Value<R, K>) => FilterExpr;
19
+ gt: <K extends Fields<R>>(name: K, value: Value<R, K>) => FilterExpr;
20
+ gte: <K extends Fields<R>>(name: K, value: Value<R, K>) => FilterExpr;
21
+ between: <K extends Fields<R>>(name: K, low: Value<R, K>, high: Value<R, K>) => FilterExpr;
22
+ has: <K extends Fields<R>>(name: K, value: Scalar) => FilterExpr;
23
+ present: <K extends Fields<R>>(name: K) => FilterExpr;
24
+ in: <K extends Fields<R>>(name: K, values: Extract<NonNullable<RecordOf<R["$fields"]>[K]>, Scalar>[]) => FilterExpr;
25
+ isNull: <K extends Fields<R>>(name: K) => FilterExpr;
26
+ search: (text: string) => FilterExpr;
27
+ similar: <K extends Fields<R>>(name: K, text: string) => FilterExpr;
28
+ regex: <K extends Fields<R>>(name: K, pattern: string) => FilterExpr;
29
+ and: (...args: FilterExpr[]) => FilterExpr;
30
+ or: (...args: FilterExpr[]) => FilterExpr;
31
+ not: (arg: FilterExpr) => FilterExpr;
32
+ now: () => FilterValue;
33
+ ago: (duration: string) => FilterValue;
34
+ fromNow: (duration: string) => FilterValue;
35
+ };
36
+ export declare const where: {
37
+ eq: <K extends string>(name: K, value: any) => FilterExpr;
38
+ ne: <K extends string>(name: K, value: any) => FilterExpr;
39
+ lt: <K extends string>(name: K, value: any) => FilterExpr;
40
+ lte: <K extends string>(name: K, value: any) => FilterExpr;
41
+ gt: <K extends string>(name: K, value: any) => FilterExpr;
42
+ gte: <K extends string>(name: K, value: any) => FilterExpr;
43
+ between: <K extends string>(name: K, low: any, high: any) => FilterExpr;
44
+ has: <K extends string>(name: K, value: Scalar) => FilterExpr;
45
+ present: <K extends string>(name: K) => FilterExpr;
46
+ in: <K extends string>(name: K, values: any[]) => FilterExpr;
47
+ isNull: <K extends string>(name: K) => FilterExpr;
48
+ search: (text: string) => FilterExpr;
49
+ similar: <K extends string>(name: K, text: string) => FilterExpr;
50
+ regex: <K extends string>(name: K, pattern: string) => FilterExpr;
51
+ and: (...args: FilterExpr[]) => FilterExpr;
52
+ or: (...args: FilterExpr[]) => FilterExpr;
53
+ not: (arg: FilterExpr) => FilterExpr;
54
+ now: () => FilterValue;
55
+ ago: (duration: string) => FilterValue;
56
+ fromNow: (duration: string) => FilterValue;
57
+ };
58
+ //#endregion
@@ -0,0 +1,121 @@
1
+ import { parseDurationText } from "./duration.js";
2
+ //#region src/filter/where.ts
3
+ const span = {
4
+ start: 0,
5
+ end: 0
6
+ };
7
+ const field = (name) => ({
8
+ kind: "field",
9
+ path: name.split("."),
10
+ span
11
+ });
12
+ const literal = (value) => {
13
+ if (typeof value === "string") return {
14
+ kind: "string",
15
+ value,
16
+ span
17
+ };
18
+ if (typeof value === "boolean") return {
19
+ kind: "boolean",
20
+ value,
21
+ span
22
+ };
23
+ if (!Number.isFinite(value)) throw new Error("Filter numbers must be finite");
24
+ return {
25
+ kind: "number",
26
+ value,
27
+ raw: String(value),
28
+ span
29
+ };
30
+ };
31
+ const filterValue = (value) => typeof value === "object" ? value : literal(value);
32
+ const group = (kind, args) => {
33
+ if (args.length === 0) throw new Error(`where.${kind} needs at least one filter`);
34
+ if (args.length === 1) return args[0];
35
+ return {
36
+ kind,
37
+ args,
38
+ span
39
+ };
40
+ };
41
+ const createWhere = () => {
42
+ const compare = (op) => (name, value) => ({
43
+ kind: "compare",
44
+ op,
45
+ field: field(name),
46
+ value: filterValue(value),
47
+ span
48
+ });
49
+ const offset = (sign) => (duration) => ({
50
+ kind: "now",
51
+ offset: {
52
+ sign,
53
+ duration: parseDurationText(duration)
54
+ },
55
+ span
56
+ });
57
+ return {
58
+ eq: compare("="),
59
+ ne: compare("!="),
60
+ lt: compare("<"),
61
+ lte: compare("<="),
62
+ gt: compare(">"),
63
+ gte: compare(">="),
64
+ between: (name, low, high) => group("and", [compare(">=")(name, low), compare("<=")(name, high)]),
65
+ has: (name, value) => ({
66
+ kind: "has",
67
+ field: field(name),
68
+ value: literal(value),
69
+ span
70
+ }),
71
+ present: (name) => ({
72
+ kind: "present",
73
+ field: field(name),
74
+ span
75
+ }),
76
+ in: (name, values) => ({
77
+ kind: "in",
78
+ field: field(name),
79
+ values: values.map(literal),
80
+ span
81
+ }),
82
+ isNull: (name) => ({
83
+ kind: "isNull",
84
+ field: field(name),
85
+ span
86
+ }),
87
+ search: (text) => ({
88
+ kind: "search",
89
+ text,
90
+ span
91
+ }),
92
+ similar: (name, text) => ({
93
+ kind: "similar",
94
+ field: field(name),
95
+ text,
96
+ span
97
+ }),
98
+ regex: (name, pattern) => ({
99
+ kind: "regex",
100
+ field: field(name),
101
+ pattern,
102
+ span
103
+ }),
104
+ and: (...args) => group("and", args),
105
+ or: (...args) => group("or", args),
106
+ not: (arg) => ({
107
+ kind: "not",
108
+ arg,
109
+ span
110
+ }),
111
+ now: () => ({
112
+ kind: "now",
113
+ span
114
+ }),
115
+ ago: offset("-"),
116
+ fromNow: offset("+")
117
+ };
118
+ };
119
+ const where = createWhere();
120
+ //#endregion
121
+ export { createWhere, where };
@@ -0,0 +1,28 @@
1
+ import { ActionModel, ActionRun, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, DatePreset, DurationLiteral, DurationUnit, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FilterError, FilterErrorCode, FilterExpr, FilterOf, FilterValue, FilterWidgetModel, KeyValue, LayoutItemModel, ListModel, ListQuery, Literal, NavModel, NavRecentModel, NowValue, OrderByItem, RelatedModel, ResourceModel, SaveFeedback, SearchMatch, SearchResultModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
2
+ import { AccessAction, AccessContext, AccessFn, AccessResult, AccessUser, FieldAccess, FieldRule } from "./access/types.js";
3
+ import { Field } from "./field.js";
4
+ import { f } from "./fields.js";
5
+ import { InferRecord, RecordKey } from "./resource-types.js";
6
+ import { resource } from "./resource.js";
7
+ import { view } from "./view.js";
8
+ import { enumLabel } from "./enum-label.js";
9
+ import { SearchTerm, matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
10
+ import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
11
+ import { UserMenuSource, userMenu } from "./user-menu.js";
12
+ import { layout } from "./layout.js";
13
+ import { KeyType, decodeKey, encodeKey, keyTypes } from "./keys.js";
14
+ import { FilterLimits, FilterParse, parseFilter } from "./filter/parse.js";
15
+ import { CheckResult, checkFilter } from "./filter/check.js";
16
+ import { EvaluateOptions, evaluateFilter } from "./filter/evaluate.js";
17
+ import { printFilter } from "./filter/print.js";
18
+ import { OrderByCheck, OrderByParse, checkOrderBy, parseOrderBy, printOrderBy } from "./filter/order-by.js";
19
+ import { createWhere, where } from "./filter/where.js";
20
+ import "./filter/index.js";
21
+ import { Capability, CapabilityAction, CapabilityScope } from "./access/capability.js";
22
+ import { Rule, rule } from "./access/rule.js";
23
+ import { Roles, RolesOptions, defineRoles } from "./access/roles.js";
24
+ import { pickView } from "./access/pick-view.js";
25
+ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, accessWarnings, checkRecord, fieldsUsedBy, resolveAccess } from "./access/resolve.js";
26
+ import { restrictModel } from "./access/restrict.js";
27
+ import "./access/index.js";
28
+ export { type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionModel, type ActionRun, type Capability, type CapabilityAction, type CapabilityScope, type ChartGranularity, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CompareOp, type DatePreset, type DurationLiteral, type DurationUnit, type EvaluateOptions, type Field, type FieldAccess, type FieldFormat, type FieldModel, type FieldPath, type FieldRule, type FieldType, type FieldViewModel, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type InferRecord, type KeyType, type KeyValue, type LayoutItemModel, type ListModel, type ListQuery, type Literal, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type RecordKey, type RelatedModel, type ResolveOptions, type ResolvedAccess, type ResourceModel, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SearchMatch, type SearchResultModel, type SearchTerm, type SortSpec, type Span, type StatusTone, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type ViewModel, accessWarnings, checkFilter, checkOrderBy, checkRecord, createWhere, decodeKey, defaultRecentLimit, defineRoles, encodeKey, enumLabel, evaluateFilter, f, fieldsUsedBy, keyTypes, layout as l, matchedSearchField, matchesSearch, navRecentErrors, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, resolveAccess, resource, restrictModel, rule, searchTerms, statusTones, termMatches, userMenu, view, where };
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import { f } from "./fields.js";
2
+ import { resource } from "./resource.js";
3
+ import { parseFilter } from "./filter/parse.js";
4
+ import { checkFilter } from "./filter/check.js";
5
+ import { evaluateFilter } from "./filter/evaluate.js";
6
+ import { printFilter } from "./filter/print.js";
7
+ import { checkOrderBy, parseOrderBy, printOrderBy } from "./filter/order-by.js";
8
+ import { createWhere, where } from "./filter/where.js";
9
+ import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
10
+ import { view } from "./view.js";
11
+ import { enumLabel } from "./enum-label.js";
12
+ import { matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
13
+ import { userMenu } from "./user-menu.js";
14
+ import { layout } from "./layout.js";
15
+ import { decodeKey, encodeKey, keyTypes } from "./keys.js";
16
+ import { rule } from "./access/rule.js";
17
+ import { defineRoles } from "./access/roles.js";
18
+ import { pickView } from "./access/pick-view.js";
19
+ import { restrictModel } from "./access/restrict.js";
20
+ import { accessWarnings, checkRecord, fieldsUsedBy, resolveAccess } from "./access/resolve.js";
21
+ export { accessWarnings, checkFilter, checkOrderBy, checkRecord, createWhere, decodeKey, defaultRecentLimit, defineRoles, encodeKey, enumLabel, evaluateFilter, f, fieldsUsedBy, keyTypes, layout as l, matchedSearchField, matchesSearch, navRecentErrors, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, resolveAccess, resource, restrictModel, rule, searchTerms, statusTones, termMatches, userMenu, view, where };
package/dist/keys.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { FieldType, KeyValue, ResourceModel } from "./model.js";
2
+ //#region src/keys.d.ts
3
+ export type KeyType = Extract<FieldType, 'integer' | 'bigint' | 'uuid' | 'text'>;
4
+ export declare const encodeKey: (key: KeyValue | readonly KeyValue[]) => string;
5
+ export declare const decodeKey: (encoded: string, types: readonly KeyType[]) => string | number | (string | number)[];
6
+ /**
7
+ * Key part types of a resource's primary key. A relation part takes the key types of the
8
+ * target resource (a composite target contributes one part per key column), so `resolve`
9
+ * is required whenever a relation is part of the key.
10
+ */
11
+ export declare const keyTypes: (model: ResourceModel, resolve?: (resource: string) => ResourceModel | undefined, seen?: string[]) => KeyType[];
12
+ //#endregion
package/dist/keys.js ADDED
@@ -0,0 +1,57 @@
1
+ //#region src/keys.ts
2
+ const integerPattern = /^-?\d+$/;
3
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4
+ const decodePart = (part, type) => {
5
+ if (type === "text") return part;
6
+ if (type === "uuid") {
7
+ if (!uuidPattern.test(part)) throw new Error("Invalid uuid key part");
8
+ return part;
9
+ }
10
+ if (!integerPattern.test(part)) throw new Error(`Invalid ${type} key part`);
11
+ if (type === "bigint") return part;
12
+ const value = Number(part);
13
+ if (!Number.isSafeInteger(value)) throw new Error("Invalid integer key part");
14
+ return value;
15
+ };
16
+ const decodeComponent = (part) => {
17
+ try {
18
+ return decodeURIComponent(part);
19
+ } catch (error) {
20
+ if (error instanceof URIError) throw new Error("Malformed key encoding");
21
+ throw error;
22
+ }
23
+ };
24
+ const encodeKey = (key) => {
25
+ if (!Array.isArray(key)) return String(key);
26
+ const parts = key;
27
+ if (parts.length === 1) return String(parts[0]);
28
+ return parts.map((part) => encodeURIComponent(String(part))).join(",");
29
+ };
30
+ const decodeKey = (encoded, types) => {
31
+ if (types.length === 0) throw new Error("A key needs at least one part");
32
+ if (types.length === 1) return decodePart(encoded, types[0]);
33
+ const parts = encoded.split(",");
34
+ if (parts.length !== types.length) throw new Error(`Expected ${types.length} key parts, got ${parts.length}`);
35
+ return parts.map((part, i) => decodePart(decodeComponent(part), types[i]));
36
+ };
37
+ const scalarKey = (type) => type === "text" || type === "integer" || type === "bigint" || type === "uuid";
38
+ /**
39
+ * Key part types of a resource's primary key. A relation part takes the key types of the
40
+ * target resource (a composite target contributes one part per key column), so `resolve`
41
+ * is required whenever a relation is part of the key.
42
+ */
43
+ const keyTypes = (model, resolve, seen = []) => model.primaryKey.flatMap((name) => {
44
+ const field = model.fields[name];
45
+ if (scalarKey(field?.type)) return [field.type];
46
+ if (field?.type !== "relation" || !field.relation) throw new Error(`Unsupported primary key type for "${name}"`);
47
+ const { resource, columns } = field.relation;
48
+ if (!resolve) throw new Error(`keyTypes needs a resolver for relation "${name}" to "${resource}"`);
49
+ const target = resolve(resource);
50
+ if (!target) throw new Error(`Unknown resource "${resource}" for relation "${name}"`);
51
+ if (seen.includes(target.name)) throw new Error(`Circular primary key through "${resource}"`);
52
+ const types = keyTypes(target, resolve, [...seen, model.name]);
53
+ if (types.length !== columns.length) throw new Error(`Relation "${name}" has ${columns.length} column(s) but "${resource}" has a ${types.length}-part key`);
54
+ return types;
55
+ });
56
+ //#endregion
57
+ export { decodeKey, encodeKey, keyTypes };
@@ -0,0 +1,12 @@
1
+ import { LayoutItemModel, RelatedModel } from "./model.js";
2
+ import { Ref } from "./refs.js";
3
+ //#region src/layout.d.ts
4
+ export declare const layout: {
5
+ section: (title: string, fields: Ref[], o?: {
6
+ help?: string;
7
+ }) => LayoutItemModel;
8
+ sidebar: (fields: Ref[]) => LayoutItemModel;
9
+ /** A table of the records of another resource that point at this one, directly or through a join table. */
10
+ related: (title: string, o: Omit<RelatedModel, "kind" | "title">) => LayoutItemModel;
11
+ };
12
+ //#endregion
package/dist/layout.js ADDED
@@ -0,0 +1,21 @@
1
+ //#region src/layout.ts
2
+ const layout = {
3
+ section: (title, fields, o = {}) => ({
4
+ kind: "section",
5
+ title,
6
+ fields: fields.map((ref) => ref.name),
7
+ ...o.help !== void 0 && { help: o.help }
8
+ }),
9
+ sidebar: (fields) => ({
10
+ kind: "sidebar",
11
+ fields: fields.map((ref) => ref.name)
12
+ }),
13
+ /** A table of the records of another resource that point at this one, directly or through a join table. */
14
+ related: (title, o) => ({
15
+ kind: "related",
16
+ title,
17
+ ...structuredClone(o)
18
+ })
19
+ };
20
+ //#endregion
21
+ export { layout };