@protobase/schema 0.0.0-stage → 0.1.1

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,55 @@
1
+ import { orResults, where } from "./combine.js";
2
+ import { grantsOf, parseCapability } from "./capability.js";
3
+ import { rule } from "./rule.js";
4
+ //#region src/access/roles.ts
5
+ const rolesOf = (user) => user.roles ?? [];
6
+ /**
7
+ * Defines roles as bundles of capability strings. Pass the resource and area names as the type
8
+ * parameter to reject unknown names at compile time:
9
+ * `defineRoles<'orders' | 'costs'>({ sales: ['orders.read.own'] })`.
10
+ */
11
+ const defineRoles = (definitions, options = {}) => {
12
+ for (const [role, capabilities] of Object.entries(definitions)) for (const capability of capabilities) {
13
+ const { name } = parseCapability(capability);
14
+ if (options.names && name !== "*" && !options.names.includes(name)) throw new Error(`Role "${role}" grants "${capability}" but "${name}" is not a known resource or area`);
15
+ }
16
+ /** Scopes granted by the user's roles for a capability, without applying them. */
17
+ const grants = (user, capability) => {
18
+ const wanted = parseCapability(capability);
19
+ const held = rolesOf(user).flatMap((role) => definitions[role] ?? []);
20
+ return new Set(held.flatMap((capability) => grantsOf(capability, wanted)));
21
+ };
22
+ const scoped = async (ctx, scope, capability) => {
23
+ const owner = ctx.model?.owner;
24
+ if (!owner) throw new Error(`"${capability}" is scoped to ${scope} rows but ${ctx.model ? `resource "${ctx.model.name}" has no .owner(...)` : "the access context has no model"}`);
25
+ if (scope === "own") return ctx.user?.id === void 0 ? false : where.eq(owner, ctx.user.id);
26
+ if (!options.teamOf) throw new Error(`"${capability}" needs a teamOf resolver in defineRoles options`);
27
+ const ids = await options.teamOf(ctx.user ?? {});
28
+ return ids.length === 0 ? false : where.in(owner, [...ids]);
29
+ };
30
+ return {
31
+ definitions,
32
+ grants,
33
+ /** Whether the user holds the capability at all, in any scope. For areas and field rules. */
34
+ has: (user, capability) => grants(user, capability).size > 0,
35
+ /**
36
+ * A rule that allows when the user's roles grant the capability: `true` for an unscoped
37
+ * grant, a row filter for `.own` and `.team`, `false` otherwise.
38
+ */
39
+ can: (capability) => {
40
+ parseCapability(capability);
41
+ return rule(async (ctx) => {
42
+ const held = grants(ctx.user ?? {}, capability);
43
+ if (held.has("all")) return true;
44
+ return (await Promise.all([...held].map((scope) => scoped(ctx, scope, capability)))).reduce((result, filter) => orResults(result, filter), false);
45
+ });
46
+ },
47
+ /** A rule that allows when the user has any of the roles. */
48
+ is: (...roles) => {
49
+ for (const role of roles) if (!Object.hasOwn(definitions, role)) throw new Error(`Unknown role "${role}"`);
50
+ return rule((ctx) => rolesOf(ctx.user ?? {}).some((role) => roles.includes(role)));
51
+ }
52
+ };
53
+ };
54
+ //#endregion
55
+ export { defineRoles };
@@ -0,0 +1,11 @@
1
+ import { AccessFn } from "./types.js";
2
+ //#region src/access/rule.d.ts
3
+ /** An access function that can be combined with plain functions or other rules. */
4
+ export type Rule<R = any> = AccessFn<R> & {
5
+ /** Both must allow; filters are ANDed. */
6
+ and: (other: AccessFn<R>) => Rule<R>;
7
+ /** Either may allow; filters are ORed. */
8
+ or: (other: AccessFn<R>) => Rule<R>;
9
+ };
10
+ export declare const rule: <R = any>(fn: AccessFn<R>) => Rule<R>;
11
+ //#endregion
@@ -0,0 +1,9 @@
1
+ import { andResults, orResults } from "./combine.js";
2
+ //#region src/access/rule.ts
3
+ const combine = (join, left, right) => async (ctx, record, input) => join(await left(ctx, record, input), await right(ctx, record, input));
4
+ const rule = (fn) => Object.assign((ctx, record, input) => fn(ctx, record, input), {
5
+ and: (other) => rule(combine(andResults, fn, other)),
6
+ or: (other) => rule(combine(orResults, fn, other))
7
+ });
8
+ //#endregion
9
+ export { rule };
@@ -0,0 +1,37 @@
1
+ import { FilterExpr, ResourceModel } from "../model.js";
2
+ //#region src/access/types.d.ts
3
+ export type AccessAction = 'list' | 'read' | 'create' | 'update' | 'delete';
4
+ /** The signed-in user as access rules see it; roles come from the auth user record. */
5
+ export type AccessUser = {
6
+ id?: string | number;
7
+ roles?: readonly string[];
8
+ [key: string]: unknown;
9
+ };
10
+ export type AccessContext = {
11
+ /** Always set by the server; optional in the type so rules stay callable with a bare context. */
12
+ user?: AccessUser;
13
+ operation?: AccessAction;
14
+ record?: unknown;
15
+ /** Set by the resolver; scoped capabilities (`.own`) need `model.owner`. */
16
+ model?: ResourceModel;
17
+ [key: string]: unknown;
18
+ };
19
+ /**
20
+ * What an access function may return: a verdict, or a filter that is ANDed into the query.
21
+ * The filter is AIP-160 text or a `FilterExpr` built with `where`.
22
+ */
23
+ export type AccessResult = boolean | string | FilterExpr;
24
+ /**
25
+ * A rule at the resource level. Without a record it decides the operation and may return a row
26
+ * filter; with `(ctx, record, input)` it is a record-level check. Rules that need the record
27
+ * may throw a `TypeError` when it is missing, which marks the operation as record-checked.
28
+ */
29
+ export type AccessFn<R = any> = (ctx: AccessContext, record?: R, input?: Partial<R>) => AccessResult | Promise<AccessResult>;
30
+ /** A field rule is role-based: it never sees a record, so readable columns are fixed per request. */
31
+ export type FieldRule = (ctx: AccessContext) => AccessResult | Promise<AccessResult>;
32
+ export type FieldAccess = {
33
+ read?: FieldRule;
34
+ create?: FieldRule;
35
+ update?: FieldRule;
36
+ };
37
+ //#endregion
@@ -0,0 +1,4 @@
1
+ //#region src/enum-label.d.ts
2
+ /** How an enum value reads: its label from the view's `valueLabels`, else the stored value unchanged (`co_signer`). */
3
+ export declare const enumLabel: (value: string, labels?: Record<string, string>) => string;
4
+ //#endregion
@@ -0,0 +1,5 @@
1
+ //#region src/enum-label.ts
2
+ /** How an enum value reads: its label from the view's `valueLabels`, else the stored value unchanged (`co_signer`). */
3
+ const enumLabel = (value, labels) => labels?.[value] ?? value;
4
+ //#endregion
5
+ export { enumLabel };
@@ -0,0 +1,69 @@
1
+ import { FieldType } from "./model.js";
2
+ import { FieldAccess } from "./access/types.js";
3
+ import { z } from "zod";
4
+ //#region src/field.d.ts
5
+ export type Constraint = 'min' | 'max' | 'regex';
6
+ export type FieldMeta = {
7
+ type: FieldType;
8
+ column?: string;
9
+ nullable: boolean;
10
+ readOnly: boolean;
11
+ filterable: boolean;
12
+ sortable: boolean;
13
+ aliases: string[];
14
+ hasDefault: boolean;
15
+ defaultValue?: unknown;
16
+ dbDefault?: boolean;
17
+ access?: FieldAccess;
18
+ sensitive?: boolean;
19
+ enumValues?: string[];
20
+ relation?: {
21
+ resource: string;
22
+ columns?: string[];
23
+ };
24
+ precision?: number;
25
+ scale?: number;
26
+ min?: number;
27
+ max?: number;
28
+ regex?: {
29
+ re: RegExp;
30
+ message: string;
31
+ };
32
+ };
33
+ export type FieldTypeDef = {
34
+ type: FieldType;
35
+ constraints: readonly Constraint[];
36
+ build: (meta: FieldMeta) => z.ZodType;
37
+ };
38
+ export declare class Field<V> {
39
+ readonly def: FieldTypeDef;
40
+ readonly meta: FieldMeta;
41
+ readonly schema: z.ZodType<V>;
42
+ constructor(def: FieldTypeDef, meta: FieldMeta);
43
+ column(name: string): Field<V>;
44
+ readOnly(): Field<V>;
45
+ optional(): Field<V | null>;
46
+ default(value: V): Field<V>;
47
+ /**
48
+ * Role-based rules for reading and writing this column. They never see a record, so the set of
49
+ * readable columns is fixed per request. A field without a rule follows its resource.
50
+ */
51
+ access(rules: FieldAccess): Field<V>;
52
+ /**
53
+ * Hidden like a password: never part of a list, record, search or write response. A caller who may read it fetches the
54
+ * value of one record on its own (`POST /{resource}/{key}:reveal`), and every reveal is published as an audit event.
55
+ */
56
+ sensitive(): Field<V>;
57
+ /** The database supplies a value when none is given (identity, now(), ...). */
58
+ dbDefault(): Field<V>;
59
+ alias(...names: string[]): Field<V>;
60
+ filterable(): Field<V>;
61
+ sortable(): Field<V>;
62
+ regex(re: RegExp, message: string): Field<V>;
63
+ min(value: number): Field<V>;
64
+ max(value: number): Field<V>;
65
+ protected with(patch: Partial<FieldMeta>): Field<V>;
66
+ private constrain;
67
+ }
68
+ export type AnyField = Field<any>;
69
+ //#endregion
package/dist/field.js ADDED
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ //#region src/field.ts
3
+ const fieldRegistry = z.registry();
4
+ var Field = class Field {
5
+ def;
6
+ meta;
7
+ schema;
8
+ constructor(def, meta) {
9
+ this.def = def;
10
+ this.meta = meta;
11
+ let schema = def.build(meta);
12
+ if (meta.nullable) schema = schema.nullable();
13
+ if (meta.hasDefault) schema = schema.default(meta.defaultValue);
14
+ this.schema = schema.register(fieldRegistry, meta);
15
+ }
16
+ column(name) {
17
+ return this.with({ column: name });
18
+ }
19
+ readOnly() {
20
+ return this.with({ readOnly: true });
21
+ }
22
+ optional() {
23
+ return this.with({ nullable: true });
24
+ }
25
+ default(value) {
26
+ return this.with({
27
+ hasDefault: true,
28
+ defaultValue: value
29
+ });
30
+ }
31
+ /**
32
+ * Role-based rules for reading and writing this column. They never see a record, so the set of
33
+ * readable columns is fixed per request. A field without a rule follows its resource.
34
+ */
35
+ access(rules) {
36
+ return this.with({ access: {
37
+ ...this.meta.access,
38
+ ...rules
39
+ } });
40
+ }
41
+ /**
42
+ * Hidden like a password: never part of a list, record, search or write response. A caller who may read it fetches the
43
+ * value of one record on its own (`POST /{resource}/{key}:reveal`), and every reveal is published as an audit event.
44
+ */
45
+ sensitive() {
46
+ return this.with({ sensitive: true });
47
+ }
48
+ /** The database supplies a value when none is given (identity, now(), ...). */
49
+ dbDefault() {
50
+ return this.with({ dbDefault: true });
51
+ }
52
+ alias(...names) {
53
+ return this.with({ aliases: [...this.meta.aliases, ...names] });
54
+ }
55
+ filterable() {
56
+ return this.with({ filterable: true });
57
+ }
58
+ sortable() {
59
+ return this.with({ sortable: true });
60
+ }
61
+ regex(re, message) {
62
+ return this.constrain("regex", { regex: {
63
+ re,
64
+ message
65
+ } });
66
+ }
67
+ min(value) {
68
+ return this.constrain("min", { min: value });
69
+ }
70
+ max(value) {
71
+ return this.constrain("max", { max: value });
72
+ }
73
+ with(patch) {
74
+ return new Field(this.def, {
75
+ ...this.meta,
76
+ ...patch
77
+ });
78
+ }
79
+ constrain(constraint, patch) {
80
+ if (!this.def.constraints.includes(constraint)) throw new Error(`f.${this.def.type} does not support .${constraint}()`);
81
+ return this.with(patch);
82
+ }
83
+ };
84
+ //#endregion
85
+ export { Field, fieldRegistry };
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/bigint.ts
3
+ const digits = /^-?\d+$/;
4
+ const min = -(2n ** 63n);
5
+ const max = 2n ** 63n - 1n;
6
+ const bigint = {
7
+ type: "bigint",
8
+ constraints: [],
9
+ build: () => z.string().refine((v) => digits.test(v) && BigInt(v) >= min && BigInt(v) <= max, { message: "Expected a 64-bit integer as a string" })
10
+ };
11
+ //#endregion
12
+ export { bigint };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/boolean.ts
3
+ const boolean = {
4
+ type: "boolean",
5
+ constraints: [],
6
+ build: () => z.boolean()
7
+ };
8
+ //#endregion
9
+ export { boolean };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/country.ts
3
+ const country = {
4
+ type: "country",
5
+ constraints: [],
6
+ build: () => z.string().regex(/^[A-Z]{2}$/, "Expected an ISO 3166-1 alpha-2 country code")
7
+ };
8
+ //#endregion
9
+ export { country };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/currency.ts
3
+ const currency = {
4
+ type: "currency",
5
+ constraints: [],
6
+ build: () => z.string().regex(/^[A-Z]{3}$/, "Expected an ISO 4217 currency code")
7
+ };
8
+ //#endregion
9
+ export { currency };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/date.ts
3
+ const date = {
4
+ type: "date",
5
+ constraints: [],
6
+ build: () => z.iso.date()
7
+ };
8
+ //#endregion
9
+ export { date };
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/decimal.ts
3
+ const shape = /^-?(\d*)(?:\.(\d+))?$/;
4
+ const decimal = {
5
+ type: "decimal",
6
+ constraints: [],
7
+ build: (o) => {
8
+ if (o.precision === void 0 || o.scale === void 0) throw new Error("f.decimal requires precision and scale");
9
+ const maxInteger = o.precision - o.scale;
10
+ const maxFraction = o.scale;
11
+ return z.string().refine((v) => {
12
+ const match = shape.exec(v);
13
+ if (!match) return false;
14
+ const integer = (match[1] ?? "").replace(/^0+/, "");
15
+ const fraction = match[2] ?? "";
16
+ if (!(match[1] || fraction)) return false;
17
+ return integer.length <= maxInteger && fraction.length <= maxFraction;
18
+ }, { message: `Expected a decimal string with at most ${o.precision} digits, ${o.scale} after the point` });
19
+ }
20
+ };
21
+ //#endregion
22
+ export { decimal };
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/enum.ts
3
+ const enumeration = {
4
+ type: "enum",
5
+ constraints: [],
6
+ build: (o) => {
7
+ if (!o.enumValues?.length) throw new Error("f.enum requires at least one value");
8
+ return z.enum(o.enumValues);
9
+ }
10
+ };
11
+ //#endregion
12
+ export { enumeration };
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/integer.ts
3
+ const integer = {
4
+ type: "integer",
5
+ constraints: ["min", "max"],
6
+ build: (o) => {
7
+ let schema = z.int();
8
+ if (o.min !== void 0) schema = schema.min(o.min);
9
+ if (o.max !== void 0) schema = schema.max(o.max);
10
+ return schema;
11
+ }
12
+ };
13
+ //#endregion
14
+ export { integer };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/json.ts
3
+ const json = {
4
+ type: "json",
5
+ constraints: [],
6
+ build: () => z.json()
7
+ };
8
+ //#endregion
9
+ export { json };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/relation.ts
3
+ const relation = {
4
+ type: "relation",
5
+ constraints: [],
6
+ build: () => z.union([z.string(), z.int()])
7
+ };
8
+ //#endregion
9
+ export { relation };
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/text.ts
3
+ const text = {
4
+ type: "text",
5
+ constraints: [
6
+ "min",
7
+ "max",
8
+ "regex"
9
+ ],
10
+ build: (o) => {
11
+ let schema = z.string();
12
+ if (o.min !== void 0) schema = schema.min(o.min);
13
+ if (o.max !== void 0) schema = schema.max(o.max);
14
+ if (o.regex) schema = schema.regex(o.regex.re, o.regex.message);
15
+ return schema;
16
+ }
17
+ };
18
+ //#endregion
19
+ export { text };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/timestamp.ts
3
+ const timestamp = {
4
+ type: "timestamp",
5
+ constraints: [],
6
+ build: () => z.iso.datetime({ offset: true })
7
+ };
8
+ //#endregion
9
+ export { timestamp };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ //#region src/fields/uuid.ts
3
+ const uuid = {
4
+ type: "uuid",
5
+ constraints: [],
6
+ build: () => z.uuid()
7
+ };
8
+ //#endregion
9
+ export { uuid };
@@ -0,0 +1,23 @@
1
+ import { Field } from "./field.js";
2
+ import { RelationField } from "./relation-field.js";
3
+ import { z } from "zod";
4
+ //#region src/fields.d.ts
5
+ export declare const f: {
6
+ text: () => Field<string>;
7
+ integer: () => Field<number>;
8
+ bigint: () => Field<string>;
9
+ decimal: (o: {
10
+ precision: number;
11
+ scale: number;
12
+ }) => Field<string>;
13
+ boolean: () => Field<boolean>;
14
+ date: () => Field<string>;
15
+ timestamp: () => Field<string>;
16
+ uuid: () => Field<string>;
17
+ enum: <const T extends readonly string[]>(values: T) => Field<T[number]>;
18
+ json: () => Field<z.core.util.JSONType>;
19
+ relation: (resource: string) => RelationField<string | number>;
20
+ currency: () => Field<string>;
21
+ country: () => Field<string>;
22
+ };
23
+ //#endregion
package/dist/fields.js ADDED
@@ -0,0 +1,49 @@
1
+ import { bigint } from "./fields/bigint.js";
2
+ import { boolean } from "./fields/boolean.js";
3
+ import { country } from "./fields/country.js";
4
+ import { currency } from "./fields/currency.js";
5
+ import { date } from "./fields/date.js";
6
+ import { decimal } from "./fields/decimal.js";
7
+ import { enumeration } from "./fields/enum.js";
8
+ import { integer } from "./fields/integer.js";
9
+ import { json } from "./fields/json.js";
10
+ import { relation } from "./fields/relation.js";
11
+ import { text } from "./fields/text.js";
12
+ import { timestamp } from "./fields/timestamp.js";
13
+ import { uuid } from "./fields/uuid.js";
14
+ import { Field } from "./field.js";
15
+ import { RelationField } from "./relation-field.js";
16
+ //#region src/fields.ts
17
+ const baseMeta = (def) => ({
18
+ type: def.type,
19
+ nullable: false,
20
+ readOnly: false,
21
+ filterable: false,
22
+ sortable: false,
23
+ aliases: [],
24
+ hasDefault: false
25
+ });
26
+ const make = (def, extra = {}) => new Field(def, {
27
+ ...baseMeta(def),
28
+ ...extra
29
+ });
30
+ const f = {
31
+ text: () => make(text),
32
+ integer: () => make(integer),
33
+ bigint: () => make(bigint),
34
+ decimal: (o) => make(decimal, o),
35
+ boolean: () => make(boolean),
36
+ date: () => make(date),
37
+ timestamp: () => make(timestamp),
38
+ uuid: () => make(uuid),
39
+ enum: (values) => make(enumeration, { enumValues: [...values] }),
40
+ json: () => make(json),
41
+ relation: (resource) => new RelationField(relation, {
42
+ ...baseMeta(relation),
43
+ relation: { resource }
44
+ }),
45
+ currency: () => make(currency),
46
+ country: () => make(country)
47
+ };
48
+ //#endregion
49
+ export { f };
@@ -0,0 +1,25 @@
1
+ import { filterError, suggest } from "./errors.js";
2
+ //#region src/filter/check-field.ts
3
+ const findField = (model, name) => Object.hasOwn(model.fields, name) ? model.fields[name] : Object.values(model.fields).find((field) => field.aliases.includes(name));
4
+ const resolveField = (ctx, path, capability) => {
5
+ if (path.path.length > 1) {
6
+ ctx.errors.push(filterError("unsupported-traversal", `Traversal into "${path.path.join(".")}" is not supported yet`, "Filter on fields of this resource only", path.span));
7
+ return;
8
+ }
9
+ const name = path.path[0];
10
+ const field = findField(ctx.model, name);
11
+ if (!field) {
12
+ const usable = Object.values(ctx.model.fields).filter((f) => f[capability]).map((f) => f.name);
13
+ const close = suggest(name, usable);
14
+ ctx.errors.push(filterError("unknown-field", `Unknown field "${name}"`, close ? `Did you mean "${close}"?` : `Available fields: ${usable.join(", ")}`, path.span));
15
+ return;
16
+ }
17
+ if (!field[capability]) {
18
+ const method = capability === "filterable" ? "filterable" : "sortable";
19
+ ctx.errors.push(filterError(capability === "filterable" ? "not-filterable" : "not-sortable", `Field "${field.name}" is not ${method}`, `Mark it with .${method}() in data.ts`, path.span));
20
+ return;
21
+ }
22
+ return field;
23
+ };
24
+ //#endregion
25
+ export { findField, resolveField };