@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.
- package/LICENSE +202 -0
- package/dist/access/capability.d.ts +10 -0
- package/dist/access/capability.js +30 -0
- package/dist/access/combine.js +39 -0
- package/dist/access/index.d.ts +8 -0
- package/dist/access/pick-view.d.ts +8 -0
- package/dist/access/pick-view.js +11 -0
- package/dist/access/resolve.d.ts +61 -0
- package/dist/access/resolve.js +150 -0
- package/dist/access/restrict.d.ts +16 -0
- package/dist/access/restrict.js +37 -0
- package/dist/access/roles.d.ts +30 -0
- package/dist/access/roles.js +55 -0
- package/dist/access/rule.d.ts +11 -0
- package/dist/access/rule.js +9 -0
- package/dist/access/types.d.ts +37 -0
- package/dist/enum-label.d.ts +4 -0
- package/dist/enum-label.js +5 -0
- package/dist/field.d.ts +69 -0
- package/dist/field.js +85 -0
- package/dist/fields/bigint.js +12 -0
- package/dist/fields/boolean.js +9 -0
- package/dist/fields/country.js +9 -0
- package/dist/fields/currency.js +9 -0
- package/dist/fields/date.js +9 -0
- package/dist/fields/decimal.js +22 -0
- package/dist/fields/enum.js +12 -0
- package/dist/fields/integer.js +14 -0
- package/dist/fields/json.js +9 -0
- package/dist/fields/relation.js +9 -0
- package/dist/fields/text.js +19 -0
- package/dist/fields/timestamp.js +9 -0
- package/dist/fields/uuid.js +9 -0
- package/dist/fields.d.ts +23 -0
- package/dist/fields.js +49 -0
- package/dist/filter/check-field.js +25 -0
- package/dist/filter/check-model.js +123 -0
- package/dist/filter/check-value.js +64 -0
- package/dist/filter/check.d.ts +16 -0
- package/dist/filter/check.js +22 -0
- package/dist/filter/datetime.js +33 -0
- package/dist/filter/duration.js +16 -0
- package/dist/filter/errors.js +29 -0
- package/dist/filter/evaluate.d.ts +12 -0
- package/dist/filter/evaluate.js +33 -0
- package/dist/filter/index.d.ts +7 -0
- package/dist/filter/lift.js +107 -0
- package/dist/filter/lower.js +170 -0
- package/dist/filter/operators.js +26 -0
- package/dist/filter/order-by.d.ts +23 -0
- package/dist/filter/order-by.js +51 -0
- package/dist/filter/parse.d.ts +20 -0
- package/dist/filter/parse.js +28 -0
- package/dist/filter/print.d.ts +5 -0
- package/dist/filter/print.js +7 -0
- package/dist/filter/profile.js +69 -0
- package/dist/filter/where.d.ts +58 -0
- package/dist/filter/where.js +121 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +21 -0
- package/dist/keys.d.ts +12 -0
- package/dist/keys.js +57 -0
- package/dist/layout.d.ts +12 -0
- package/dist/layout.js +21 -0
- package/dist/model.d.ts +333 -0
- package/dist/naming.js +4 -0
- package/dist/nav-recent.d.ts +8 -0
- package/dist/nav-recent.js +37 -0
- package/dist/refs.d.ts +11 -0
- package/dist/refs.js +8 -0
- package/dist/relation-field.d.ts +6 -0
- package/dist/relation-field.js +12 -0
- package/dist/resource-model.d.ts +15 -0
- package/dist/resource-model.js +71 -0
- package/dist/resource-types.d.ts +21 -0
- package/dist/resource.d.ts +47 -0
- package/dist/resource.js +97 -0
- package/dist/search-match.d.ts +29 -0
- package/dist/search-match.js +50 -0
- package/dist/user-menu.d.ts +25 -0
- package/dist/user-menu.js +32 -0
- package/dist/view-actions.d.ts +24 -0
- package/dist/view-actions.js +35 -0
- package/dist/view-field.d.ts +16 -0
- package/dist/view-field.js +39 -0
- package/dist/view-widgets.d.ts +16 -0
- package/dist/view-widgets.js +24 -0
- package/dist/view.d.ts +69 -0
- package/dist/view.js +109 -0
- package/package.json +31 -3
- 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
|
package/dist/field.d.ts
ADDED
|
@@ -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,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,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 };
|
package/dist/fields.d.ts
ADDED
|
@@ -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 };
|