@protobase/schema 0.13.3 → 0.14.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/capability.d.ts +3 -0
- package/dist/access/index.d.ts +3 -3
- package/dist/access/resolve.d.ts +13 -0
- package/dist/access/role-entry.d.ts +5 -0
- package/dist/access/roles.d.ts +14 -0
- package/dist/access/roles.js +11 -0
- package/dist/access/rule.d.ts +10 -0
- package/dist/access/rule.js +10 -0
- package/dist/access/types.d.ts +11 -0
- package/dist/assistant/protocol.d.ts +67 -2
- package/dist/bottom-bar-errors.d.ts +3 -0
- package/dist/bottom-bar.d.ts +3 -1
- package/dist/bottom-bar.js +2 -1
- package/dist/events/event-helpers.d.ts +41 -8
- package/dist/events/event-helpers.js +1 -3
- package/dist/field-ref.d.ts +1 -0
- package/dist/field.d.ts +54 -1
- package/dist/field.js +16 -0
- package/dist/fields.d.ts +31 -0
- package/dist/fields.js +31 -0
- package/dist/file-field.d.ts +6 -1
- package/dist/file-field.js +6 -1
- package/dist/files/file-options.d.ts +17 -0
- package/dist/filter/check.d.ts +5 -0
- package/dist/filter/evaluate.d.ts +1 -0
- package/dist/filter/order-by.d.ts +14 -0
- package/dist/filter/order-by.js +2 -0
- package/dist/filter/parse.d.ts +16 -2
- package/dist/filter/parse.js +8 -2
- package/dist/filter/where.d.ts +61 -0
- package/dist/filter/where.js +34 -0
- package/dist/index.d.ts +16 -24
- package/dist/index.js +6 -18
- package/dist/internal.d.ts +17 -0
- package/dist/internal.js +17 -0
- package/dist/keys.d.ts +1 -0
- package/dist/layout.d.ts +15 -0
- package/dist/layout.js +15 -0
- package/dist/model.d.ts +344 -6
- package/dist/refs.d.ts +6 -0
- package/dist/relation-field.d.ts +5 -0
- package/dist/relation-field.js +5 -0
- package/dist/resource-types.d.ts +18 -0
- package/dist/resource.d.ts +94 -6
- package/dist/resource.js +75 -2
- package/dist/user-menu.d.ts +5 -1
- package/dist/user-menu.js +4 -1
- package/dist/view-actions.d.ts +1 -0
- package/dist/view-displays.d.ts +3 -0
- package/dist/view-field.d.ts +14 -1
- package/dist/view-field.js +7 -0
- package/dist/view-widgets.d.ts +7 -0
- package/dist/view-widgets.js +7 -0
- package/dist/view.d.ts +52 -4
- package/dist/view.js +39 -2
- package/dist/webhooks/webhook-resources.js +1 -1
- package/package.json +3 -2
package/dist/refs.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { AnyField, Field } from "./field.js";
|
|
2
2
|
//#region src/refs.d.ts
|
|
3
|
+
/** The fields of `.fields({ ... })` by name; `null` ignores a column on purpose. */
|
|
3
4
|
export type FieldsInput = Record<string, AnyField | null>;
|
|
4
5
|
export type ActiveFields<F> = { [K in keyof F as F[K] extends null ? never : K]: F[K]; };
|
|
6
|
+
/** A field picked in a builder's callback, such as `r.status`: its name, typed with its value. */
|
|
5
7
|
export type Ref<N extends string = string, V = unknown> = {
|
|
8
|
+
/** The field's name. */
|
|
6
9
|
readonly name: N;
|
|
10
|
+
/** The field's value type, for the compiler only; never set. */
|
|
7
11
|
readonly $value?: V;
|
|
8
12
|
};
|
|
13
|
+
/** A reference per field of `F`, by name. */
|
|
9
14
|
export type Refs<F> = { readonly [K in keyof F & string]: F[K] extends Field<infer V> ? Ref<K, V> : never; };
|
|
15
|
+
/** The `r` of a builder's callback: a reference per field of `F`, the ignored (`null`) ones left out. */
|
|
10
16
|
export type FieldRefs<F> = Refs<ActiveFields<F>>;
|
|
11
17
|
//#endregion
|
package/dist/relation-field.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Field } from "./field.js";
|
|
2
2
|
//#region src/relation-field.d.ts
|
|
3
|
+
/** `f.relation(resource)`: a field holding the key of a record of another resource. */
|
|
3
4
|
export declare class RelationField<V> extends Field<V> {
|
|
5
|
+
/**
|
|
6
|
+
* The columns that hold the other record's key, in the order of that key, for a resource with a key of several
|
|
7
|
+
* columns. See [Keys and relations](website/src/content/docs/reference/data-config.mdx#keys-and-relations).
|
|
8
|
+
*/
|
|
4
9
|
columns(columns: string[]): Field<V>;
|
|
5
10
|
}
|
|
6
11
|
//#endregion
|
package/dist/relation-field.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Field } from "./field.js";
|
|
2
2
|
//#region src/relation-field.ts
|
|
3
|
+
/** `f.relation(resource)`: a field holding the key of a record of another resource. */
|
|
3
4
|
var RelationField = class extends Field {
|
|
5
|
+
/**
|
|
6
|
+
* The columns that hold the other record's key, in the order of that key, for a resource with a key of several
|
|
7
|
+
* columns. See [Keys and relations](website/src/content/docs/reference/data-config.mdx#keys-and-relations).
|
|
8
|
+
*/
|
|
4
9
|
columns(columns) {
|
|
5
10
|
return this.with({ relation: {
|
|
6
11
|
resource: this.meta.relation.resource,
|
package/dist/resource-types.d.ts
CHANGED
|
@@ -3,19 +3,37 @@ import { Field } from "./field.js";
|
|
|
3
3
|
import { ActiveFields, Ref } from "./refs.js";
|
|
4
4
|
//#region src/resource-types.d.ts
|
|
5
5
|
type Prettify<T> = { [K in keyof T]: T[K]; } & {};
|
|
6
|
+
/** The record of a resource with fields `F`: each field's value type by name, the ignored (`null`) ones left out. */
|
|
6
7
|
export type RecordOf<F> = Prettify<{ [K in keyof ActiveFields<F>]: ActiveFields<F>[K] extends Field<infer V> ? V : never; }>;
|
|
8
|
+
/** What `.primaryKey()` picks: one field, or a list for a key of several columns. */
|
|
7
9
|
export type KeyRefs = Ref | readonly Ref[];
|
|
10
|
+
/** A record's key from the fields `.primaryKey()` picked: one value, or a tuple of the parts. */
|
|
8
11
|
export type KeyOf<K> = K extends readonly Ref[] ? { -readonly [I in keyof K]: K[I] extends Ref<string, infer V> ? V : never; } : K extends Ref<string, infer V> ? V : never;
|
|
12
|
+
/** A problem `.validate()` finds with a record, answered with `400` and the field named. */
|
|
9
13
|
export type Issue<F> = {
|
|
14
|
+
/** The field it is about. */
|
|
10
15
|
field: keyof RecordOf<F> & string;
|
|
16
|
+
/** What is wrong, for the user. */
|
|
11
17
|
message: string;
|
|
12
18
|
};
|
|
13
19
|
/** Resource-level rules: operation, row filter and record-level check in one function. */
|
|
14
20
|
export type Access<F> = { [A in AccessAction]?: AccessFn<RecordOf<F>>; };
|
|
21
|
+
/**
|
|
22
|
+
* The record type of a resource, from the builder `resource(...)` returns: `InferRecord<typeof orders>`.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam R - The resource builder, whose `$fields` carries its fields.
|
|
25
|
+
*/
|
|
15
26
|
export type InferRecord<R extends {
|
|
27
|
+
/** The builder's fields, as a type only. */
|
|
16
28
|
readonly $fields: unknown;
|
|
17
29
|
}> = RecordOf<R['$fields']>;
|
|
30
|
+
/**
|
|
31
|
+
* The key type of a resource, from the builder `resource(...)` returns: `RecordKey<typeof stock>`.
|
|
32
|
+
*
|
|
33
|
+
* @typeParam R - The resource builder, whose `$key` carries the fields of its key.
|
|
34
|
+
*/
|
|
18
35
|
export type RecordKey<R extends {
|
|
36
|
+
/** The fields of the builder's key, as a type only. */
|
|
19
37
|
readonly $key: unknown;
|
|
20
38
|
}> = KeyOf<R['$key']>;
|
|
21
39
|
//#endregion
|
package/dist/resource.d.ts
CHANGED
|
@@ -7,25 +7,59 @@ import { Access, Issue, KeyRefs, RecordOf } from "./resource-types.js";
|
|
|
7
7
|
import "./index.js";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
//#region src/resource.d.ts
|
|
10
|
-
|
|
10
|
+
/** A field picked for search, with how it matches. */
|
|
11
|
+
export type SearchRef = Ref & {
|
|
12
|
+
/** How it matches when not by containing the words; see {@link SearchMatch}. */
|
|
11
13
|
readonly match?: SearchMatch;
|
|
12
14
|
};
|
|
13
15
|
/** A field picked for search; `digitsEnd()` matches only the end of its digits, ignoring spaces, `+` and dashes. */
|
|
14
|
-
type SearchRefs<F> = { readonly [K in keyof FieldRefs<F>]: FieldRefs<F>[K] & {
|
|
16
|
+
export type SearchRefs<F> = { readonly [K in keyof FieldRefs<F>]: FieldRefs<F>[K] & {
|
|
17
|
+
/** Matches only the end of the field's digits, ignoring spaces, `+` and dashes, for phone numbers. */
|
|
15
18
|
digitsEnd(): SearchRef;
|
|
16
19
|
}; };
|
|
17
|
-
|
|
20
|
+
/** A rule over a whole record for `.validate()`: the issues it finds, or `undefined` when there are none. */
|
|
21
|
+
export type Validator<F> = (record: RecordOf<F>) => Issue<F>[] | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* A resource as `resource(name)` builds it. Every method returns a new builder, so a resource is one chain. See
|
|
24
|
+
* [resource()](website/src/content/docs/reference/data-config.mdx#resource).
|
|
25
|
+
*/
|
|
18
26
|
export declare class ResourceBuilder<F extends FieldsInput = {}, K = never> {
|
|
27
|
+
/** @internal */
|
|
19
28
|
readonly state: ResourceState;
|
|
29
|
+
/** @internal */
|
|
20
30
|
readonly validators: readonly Validator<F>[];
|
|
31
|
+
/** @internal */
|
|
21
32
|
readonly accessRules: Access<F>;
|
|
33
|
+
/** The fields, for the compiler only, which {@link InferRecord} reads; never set. */
|
|
22
34
|
readonly $fields: F;
|
|
35
|
+
/** The fields of the key, for the compiler only, which {@link RecordKey} reads; never set. */
|
|
23
36
|
readonly $key: K;
|
|
24
|
-
|
|
37
|
+
/** @internal */
|
|
38
|
+
constructor(
|
|
39
|
+
/** @internal */
|
|
40
|
+
state: ResourceState,
|
|
41
|
+
/** @internal */
|
|
42
|
+
validators?: readonly Validator<F>[],
|
|
43
|
+
/** @internal */
|
|
44
|
+
accessRules?: Access<F>);
|
|
45
|
+
/** The table, with or without its schema: `'shop.repairs'` or `'repairs'`. Required. */
|
|
25
46
|
table(table: string): ResourceBuilder<F, K>;
|
|
47
|
+
/**
|
|
48
|
+
* The columns, by field name, each built with `f`. `name: null` ignores a column on purpose. The column is the
|
|
49
|
+
* snake_case of the name unless `.column()` says otherwise. Required, and called before the methods that pick fields.
|
|
50
|
+
*/
|
|
26
51
|
fields<const N extends FieldsInput>(fields: N): ResourceBuilder<N, never>;
|
|
52
|
+
/** The key: one field, or a list for a key of several columns (`(r) => [r.order_id, r.line_no]`). Required. */
|
|
27
53
|
primaryKey<const P extends KeyRefs>(pick: (r: FieldRefs<F>) => P): ResourceBuilder<F, P>;
|
|
54
|
+
/**
|
|
55
|
+
* A nullable timestamp or date that marks a record deleted: deleting sets it, lists leave such rows out and
|
|
56
|
+
* `:undelete` clears it.
|
|
57
|
+
*/
|
|
28
58
|
softDelete(pick: (r: FieldRefs<F>) => Ref): ResourceBuilder<F, K>;
|
|
59
|
+
/**
|
|
60
|
+
* The field that separates tenants: every query is limited to the caller's tenant, creates set it, and a request body
|
|
61
|
+
* may not.
|
|
62
|
+
*/
|
|
29
63
|
tenant(pick: (r: FieldRefs<F>) => Ref): ResourceBuilder<F, K>;
|
|
30
64
|
/**
|
|
31
65
|
* Fields matched by `search("...")` and bare words in filters. Authoritative over the UI's list search defaults.
|
|
@@ -35,22 +69,76 @@ export declare class ResourceBuilder<F extends FieldsInput = {}, K = never> {
|
|
|
35
69
|
/** The field holding the owning user's id, which `.own` capability scopes filter on. */
|
|
36
70
|
owner(pick: (r: FieldRefs<F>) => Ref): ResourceBuilder<F, K>;
|
|
37
71
|
/**
|
|
38
|
-
* The events changes to its records raise, captured in the database for every write, from the API or elsewhere
|
|
39
|
-
*
|
|
72
|
+
* The events changes to its records raise, captured in the database for every write, from the API or elsewhere. See
|
|
73
|
+
* [Declaring events](website/src/content/docs/reference/events.mdx#declaring-events).
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* .events((e, r) => [
|
|
78
|
+
* ...e.crud(),
|
|
79
|
+
* e.becomes('large', 'total > 1000'),
|
|
80
|
+
* e.updated('statusChanged', { changed: [r.status] }),
|
|
81
|
+
* e.updated('reopened', { from: 'status = "cancelled"', when: 'status != "cancelled"' }),
|
|
82
|
+
* ])
|
|
83
|
+
* ```
|
|
40
84
|
*/
|
|
41
85
|
events(define: (e: EventHelpers<RecordOf<F>>, r: FieldRefs<F>) => EventSpec[]): ResourceBuilder<F, K>;
|
|
42
86
|
/** The code predicates (`match`) of its events, by event name. */
|
|
43
87
|
eventMatches(): Record<string, EventMatch<any>>;
|
|
88
|
+
/**
|
|
89
|
+
* A rule over the whole record, run after the field checks on every create and update. Its issues answer `400` with
|
|
90
|
+
* the field named. Each call adds a rule.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* .validate((repair) => (repair.status === 'collected' && !repair.paid ? [{ field: 'paid', message: 'A bike leaves the shop paid' }] : undefined))
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
44
97
|
validate(validator: Validator<F>): ResourceBuilder<F, K>;
|
|
98
|
+
/**
|
|
99
|
+
* Rules per operation (`list`, `read`, `create`, `update`, `delete`). A rule returns `true`, `false`, or a filter that
|
|
100
|
+
* limits the rows. An operation without a rule needs its capability when the app has roles. See
|
|
101
|
+
* [Access](website/src/content/docs/reference/access.mdx).
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* .access({
|
|
106
|
+
* read: roles.can('work_orders.read'),
|
|
107
|
+
* update: roles.can('work_orders.update').and((_ctx, record) => (record as { status: string }).status !== 'done'),
|
|
108
|
+
* delete: roles.is('admin'),
|
|
109
|
+
* })
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
45
112
|
access(rules: Access<F>): ResourceBuilder<F, K>;
|
|
46
113
|
/** Role-based rules per field, for fields that declared `.access(...)`. */
|
|
47
114
|
fieldAccess(): Record<string, FieldAccess>;
|
|
115
|
+
/** The resource as the server and the app read it. Throws when the table, fields or key are missing, or a field option conflicts with another. */
|
|
48
116
|
toModel(): ResourceModel;
|
|
117
|
+
/** The Zod schema of a whole record, from its fields' schemas. */
|
|
49
118
|
recordSchema(): z.ZodType<RecordOf<F>>;
|
|
119
|
+
/** The columns of the fields given as `null`, which the resource ignores on purpose. */
|
|
50
120
|
ignoredColumns(): string[];
|
|
51
121
|
private refs;
|
|
52
122
|
private knownFields;
|
|
53
123
|
private next;
|
|
54
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* A resource: one table of an existing database, its columns as fields, its key and the rules around it. `name` is its
|
|
127
|
+
* address (`/api/v1/<name>`) and how other config refers to it. See
|
|
128
|
+
* [Data config](website/src/content/docs/reference/data-config.mdx).
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* export const customers = resource('customers')
|
|
133
|
+
* .table('shop.customers')
|
|
134
|
+
* .fields({
|
|
135
|
+
* id: f.integer().readOnly().filterable().sortable().dbDefault(),
|
|
136
|
+
* name: f.text().min(2).filterable().sortable(),
|
|
137
|
+
* email: f.text().optional().filterable(),
|
|
138
|
+
* })
|
|
139
|
+
* .primaryKey((r) => r.id)
|
|
140
|
+
* .search((r) => [r.name, r.email])
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
55
143
|
export declare const resource: (name: string) => ResourceBuilder<{}, never>;
|
|
56
144
|
//#endregion
|
package/dist/resource.js
CHANGED
|
@@ -4,24 +4,35 @@ import { buildResourceModel, ignoredColumns } from "./resource-model.js";
|
|
|
4
4
|
import { createRefs } from "./refs.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
//#region src/resource.ts
|
|
7
|
+
/**
|
|
8
|
+
* A resource as `resource(name)` builds it. Every method returns a new builder, so a resource is one chain. See
|
|
9
|
+
* [resource()](website/src/content/docs/reference/data-config.mdx#resource).
|
|
10
|
+
*/
|
|
7
11
|
var ResourceBuilder = class ResourceBuilder {
|
|
8
12
|
state;
|
|
9
13
|
validators;
|
|
10
14
|
accessRules;
|
|
15
|
+
/** @internal */
|
|
11
16
|
constructor(state, validators = [], accessRules = {}) {
|
|
12
17
|
this.state = state;
|
|
13
18
|
this.validators = validators;
|
|
14
19
|
this.accessRules = accessRules;
|
|
15
20
|
}
|
|
21
|
+
/** The table, with or without its schema: `'shop.repairs'` or `'repairs'`. Required. */
|
|
16
22
|
table(table) {
|
|
17
23
|
return this.next({ table });
|
|
18
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The columns, by field name, each built with `f`. `name: null` ignores a column on purpose. The column is the
|
|
27
|
+
* snake_case of the name unless `.column()` says otherwise. Required, and called before the methods that pick fields.
|
|
28
|
+
*/
|
|
19
29
|
fields(fields) {
|
|
20
30
|
return new ResourceBuilder({
|
|
21
31
|
...this.state,
|
|
22
32
|
fields
|
|
23
33
|
});
|
|
24
34
|
}
|
|
35
|
+
/** The key: one field, or a list for a key of several columns (`(r) => [r.order_id, r.line_no]`). Required. */
|
|
25
36
|
primaryKey(pick) {
|
|
26
37
|
const picked = pick(this.refs());
|
|
27
38
|
const refs = Array.isArray(picked) ? picked : [picked];
|
|
@@ -30,9 +41,17 @@ var ResourceBuilder = class ResourceBuilder {
|
|
|
30
41
|
primaryKey: refs.map((ref) => ref.name)
|
|
31
42
|
}, this.validators, this.accessRules);
|
|
32
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* A nullable timestamp or date that marks a record deleted: deleting sets it, lists leave such rows out and
|
|
46
|
+
* `:undelete` clears it.
|
|
47
|
+
*/
|
|
33
48
|
softDelete(pick) {
|
|
34
49
|
return this.next({ softDelete: pick(this.refs()).name });
|
|
35
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The field that separates tenants: every query is limited to the caller's tenant, creates set it, and a request body
|
|
53
|
+
* may not.
|
|
54
|
+
*/
|
|
36
55
|
tenant(pick) {
|
|
37
56
|
return this.next({ tenant: pick(this.refs()).name });
|
|
38
57
|
}
|
|
@@ -59,8 +78,18 @@ var ResourceBuilder = class ResourceBuilder {
|
|
|
59
78
|
return this.next({ owner: pick(this.refs()).name });
|
|
60
79
|
}
|
|
61
80
|
/**
|
|
62
|
-
* The events changes to its records raise, captured in the database for every write, from the API or elsewhere
|
|
63
|
-
*
|
|
81
|
+
* The events changes to its records raise, captured in the database for every write, from the API or elsewhere. See
|
|
82
|
+
* [Declaring events](website/src/content/docs/reference/events.mdx#declaring-events).
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* .events((e, r) => [
|
|
87
|
+
* ...e.crud(),
|
|
88
|
+
* e.becomes('large', 'total > 1000'),
|
|
89
|
+
* e.updated('statusChanged', { changed: [r.status] }),
|
|
90
|
+
* e.updated('reopened', { from: 'status = "cancelled"', when: 'status != "cancelled"' }),
|
|
91
|
+
* ])
|
|
92
|
+
* ```
|
|
64
93
|
*/
|
|
65
94
|
events(define) {
|
|
66
95
|
return this.next({ events: define(eventHelpers(), this.refs()) });
|
|
@@ -69,9 +98,32 @@ var ResourceBuilder = class ResourceBuilder {
|
|
|
69
98
|
eventMatches() {
|
|
70
99
|
return eventMatches(this.toModel(), this.state.events ?? []);
|
|
71
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* A rule over the whole record, run after the field checks on every create and update. Its issues answer `400` with
|
|
103
|
+
* the field named. Each call adds a rule.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* .validate((repair) => (repair.status === 'collected' && !repair.paid ? [{ field: 'paid', message: 'A bike leaves the shop paid' }] : undefined))
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
72
110
|
validate(validator) {
|
|
73
111
|
return new ResourceBuilder(this.state, [...this.validators, validator], this.accessRules);
|
|
74
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Rules per operation (`list`, `read`, `create`, `update`, `delete`). A rule returns `true`, `false`, or a filter that
|
|
115
|
+
* limits the rows. An operation without a rule needs its capability when the app has roles. See
|
|
116
|
+
* [Access](website/src/content/docs/reference/access.mdx).
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* .access({
|
|
121
|
+
* read: roles.can('work_orders.read'),
|
|
122
|
+
* update: roles.can('work_orders.update').and((_ctx, record) => (record as { status: string }).status !== 'done'),
|
|
123
|
+
* delete: roles.is('admin'),
|
|
124
|
+
* })
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
75
127
|
access(rules) {
|
|
76
128
|
return new ResourceBuilder(this.state, this.validators, {
|
|
77
129
|
...this.accessRules,
|
|
@@ -82,13 +134,16 @@ var ResourceBuilder = class ResourceBuilder {
|
|
|
82
134
|
fieldAccess() {
|
|
83
135
|
return Object.fromEntries(Object.entries(this.state.fields ?? {}).flatMap(([key, field]) => field?.meta.access ? [[key, field.meta.access]] : []));
|
|
84
136
|
}
|
|
137
|
+
/** The resource as the server and the app read it. Throws when the table, fields or key are missing, or a field option conflicts with another. */
|
|
85
138
|
toModel() {
|
|
86
139
|
return buildResourceModel(this.state);
|
|
87
140
|
}
|
|
141
|
+
/** The Zod schema of a whole record, from its fields' schemas. */
|
|
88
142
|
recordSchema() {
|
|
89
143
|
const shape = Object.fromEntries(Object.entries(this.state.fields ?? {}).flatMap(([key, field]) => field ? [[key, field.schema]] : []));
|
|
90
144
|
return z.object(shape);
|
|
91
145
|
}
|
|
146
|
+
/** The columns of the fields given as `null`, which the resource ignores on purpose. */
|
|
92
147
|
ignoredColumns() {
|
|
93
148
|
return ignoredColumns(this.state.fields);
|
|
94
149
|
}
|
|
@@ -105,6 +160,24 @@ var ResourceBuilder = class ResourceBuilder {
|
|
|
105
160
|
}, this.validators, this.accessRules);
|
|
106
161
|
}
|
|
107
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* A resource: one table of an existing database, its columns as fields, its key and the rules around it. `name` is its
|
|
165
|
+
* address (`/api/v1/<name>`) and how other config refers to it. See
|
|
166
|
+
* [Data config](website/src/content/docs/reference/data-config.mdx).
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* export const customers = resource('customers')
|
|
171
|
+
* .table('shop.customers')
|
|
172
|
+
* .fields({
|
|
173
|
+
* id: f.integer().readOnly().filterable().sortable().dbDefault(),
|
|
174
|
+
* name: f.text().min(2).filterable().sortable(),
|
|
175
|
+
* email: f.text().optional().filterable(),
|
|
176
|
+
* })
|
|
177
|
+
* .primaryKey((r) => r.id)
|
|
178
|
+
* .search((r) => [r.name, r.email])
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
108
181
|
const resource = (name) => new ResourceBuilder({ name });
|
|
109
182
|
//#endregion
|
|
110
183
|
export { ResourceBuilder, resource };
|
package/dist/user-menu.d.ts
CHANGED
|
@@ -18,8 +18,12 @@ declare const items: {
|
|
|
18
18
|
};
|
|
19
19
|
/** What the server needs from a `userMenu(...)` export of the config module. */
|
|
20
20
|
export type UserMenuSource = {
|
|
21
|
+
/** The menu's model, a copy each time. */
|
|
21
22
|
toUserMenuModel(): UserMenuModel;
|
|
22
23
|
};
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The pages under the signed-in user, at the bottom of the sidebar, above the built-in "Sign out": `m.resource`,
|
|
26
|
+
* `m.page` and `m.link`. Export it from the config module. See [User menu](website/src/content/docs/reference/user-menu.md).
|
|
27
|
+
*/
|
|
24
28
|
export declare const userMenu: (build: (m: typeof items) => UserMenuItemModel[]) => UserMenuSource;
|
|
25
29
|
//#endregion
|
package/dist/user-menu.js
CHANGED
|
@@ -24,7 +24,10 @@ const items = {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The pages under the signed-in user, at the bottom of the sidebar, above the built-in "Sign out": `m.resource`,
|
|
29
|
+
* `m.page` and `m.link`. Export it from the config module. See [User menu](website/src/content/docs/reference/user-menu.md).
|
|
30
|
+
*/
|
|
28
31
|
const userMenu = (build) => {
|
|
29
32
|
const model = { items: build(items) };
|
|
30
33
|
return { toUserMenuModel: () => structuredClone(model) };
|
package/dist/view-actions.d.ts
CHANGED
package/dist/view-displays.d.ts
CHANGED
|
@@ -3,11 +3,14 @@ import { Ref } from "./refs.js";
|
|
|
3
3
|
//#region src/view-displays.d.ts
|
|
4
4
|
/** A custom component as `component(name)` from `@protobase/layout` declares it. */
|
|
5
5
|
export type ComponentRef<P> = ((props: P) => unknown) & {
|
|
6
|
+
/** The name it is registered under. */
|
|
6
7
|
readonly blockName: string;
|
|
8
|
+
/** Whether it is the project's or a plugin's, rather than a built-in block. */
|
|
7
9
|
readonly custom: boolean;
|
|
8
10
|
};
|
|
9
11
|
/** A custom component's props as a view gives them: plain data, or `r.field` where it takes a field's name. */
|
|
10
12
|
export type ViewProps<P> = { [K in keyof Omit<P, 'children'>]: P[K] extends string ? P[K] | Ref : P[K]; };
|
|
13
|
+
/** @inline */
|
|
11
14
|
type Naming = {
|
|
12
15
|
name?: string;
|
|
13
16
|
label?: string;
|
package/dist/view-field.d.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { BadgeShape, FieldFormat, FieldViewModel, JsonValue } from "./model.js";
|
|
2
2
|
//#region src/view-field.d.ts
|
|
3
|
+
/** How a view shows one field, as `.fields((r) => ...)` hands it out: each hint returns a new field view. */
|
|
3
4
|
export declare class FieldView<N extends string = string, V = unknown> {
|
|
5
|
+
/** The field's name. */
|
|
4
6
|
readonly name: N;
|
|
7
|
+
/** The hints given so far. */
|
|
5
8
|
readonly props: FieldViewModel;
|
|
6
|
-
|
|
9
|
+
/** @internal */
|
|
10
|
+
constructor(
|
|
11
|
+
/** The field's name. */
|
|
12
|
+
name: N,
|
|
13
|
+
/** The hints given so far. */
|
|
14
|
+
props?: FieldViewModel);
|
|
15
|
+
/** What the field is called, instead of its name in words. */
|
|
7
16
|
label(label: string): FieldView<N, V>;
|
|
17
|
+
/** A line under the field on the record page and in forms. */
|
|
8
18
|
help(help: string): FieldView<N, V>;
|
|
19
|
+
/** How the value shows: `badge` for statuses, `code` in monospace, `percent` with a % sign. */
|
|
9
20
|
format(format: FieldFormat): FieldView<N, V>;
|
|
21
|
+
/** Text before a number wherever it is shown, such as a currency: `.prefix('€')`. */
|
|
10
22
|
prefix(prefix: string): FieldView<N, V>;
|
|
11
23
|
/** Text after a number wherever it is shown, such as a unit: `.suffix(' m²')`. */
|
|
12
24
|
suffix(suffix: string): FieldView<N, V>;
|
|
25
|
+
/** How many decimals a number shows wherever it is shown. */
|
|
13
26
|
decimals(decimals: number): FieldView<N, V>;
|
|
14
27
|
/** Labels for an enum's values, such as `{ co_signer: 'Co-signer' }`; values left out read as themselves in words. */
|
|
15
28
|
valueLabels(labels: { readonly [P in Extract<V, string>]?: string; }): FieldView<N, V>;
|
package/dist/view-field.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
//#region src/view-field.ts
|
|
2
2
|
const hexColor = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
3
|
+
/** How a view shows one field, as `.fields((r) => ...)` hands it out: each hint returns a new field view. */
|
|
3
4
|
var FieldView = class FieldView {
|
|
4
5
|
name;
|
|
5
6
|
props;
|
|
7
|
+
/** @internal */
|
|
6
8
|
constructor(name, props = {}) {
|
|
7
9
|
this.name = name;
|
|
8
10
|
this.props = props;
|
|
9
11
|
}
|
|
12
|
+
/** What the field is called, instead of its name in words. */
|
|
10
13
|
label(label) {
|
|
11
14
|
return this.with({ label });
|
|
12
15
|
}
|
|
16
|
+
/** A line under the field on the record page and in forms. */
|
|
13
17
|
help(help) {
|
|
14
18
|
return this.with({ help });
|
|
15
19
|
}
|
|
20
|
+
/** How the value shows: `badge` for statuses, `code` in monospace, `percent` with a % sign. */
|
|
16
21
|
format(format) {
|
|
17
22
|
return this.with({ format });
|
|
18
23
|
}
|
|
24
|
+
/** Text before a number wherever it is shown, such as a currency: `.prefix('€')`. */
|
|
19
25
|
prefix(prefix) {
|
|
20
26
|
return this.with({ prefix });
|
|
21
27
|
}
|
|
@@ -23,6 +29,7 @@ var FieldView = class FieldView {
|
|
|
23
29
|
suffix(suffix) {
|
|
24
30
|
return this.with({ suffix });
|
|
25
31
|
}
|
|
32
|
+
/** How many decimals a number shows wherever it is shown. */
|
|
26
33
|
decimals(decimals) {
|
|
27
34
|
return this.with({ decimals });
|
|
28
35
|
}
|
package/dist/view-widgets.d.ts
CHANGED
|
@@ -2,15 +2,22 @@ import { DatePreset, FilterWidgetModel } from "./model.js";
|
|
|
2
2
|
import { Ref } from "./refs.js";
|
|
3
3
|
//#region src/view-widgets.d.ts
|
|
4
4
|
export declare const widgets: {
|
|
5
|
+
/** A slider between the field's lowest and highest value; `histogram` draws the values' histogram behind it. */
|
|
5
6
|
range: (ref: Ref, o?: {
|
|
6
7
|
histogram?: boolean;
|
|
7
8
|
}) => FilterWidgetModel;
|
|
9
|
+
/**
|
|
10
|
+
* A menu of the field's values with counts; `search` adds a search box for long lists. On a relation it searches the
|
|
11
|
+
* referenced records as you type, on a number field it is a range slider.
|
|
12
|
+
*/
|
|
8
13
|
facets: (ref: Ref, o?: {
|
|
9
14
|
search?: boolean;
|
|
10
15
|
}) => FilterWidgetModel;
|
|
16
|
+
/** A date range with buttons for the `presets`, such as `today` and `30d`. */
|
|
11
17
|
dateRange: (ref: Ref, o?: {
|
|
12
18
|
presets?: DatePreset[];
|
|
13
19
|
}) => FilterWidgetModel;
|
|
20
|
+
/** A switch for a boolean field. */
|
|
14
21
|
toggle: (ref: Ref) => FilterWidgetModel;
|
|
15
22
|
/**
|
|
16
23
|
* A menu of boolean fields under one `label`, such as "for sale" and "for rent": a record shows when any chosen field
|
package/dist/view-widgets.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
//#region src/view-widgets.ts
|
|
2
2
|
const widgets = {
|
|
3
|
+
/** A slider between the field's lowest and highest value; `histogram` draws the values' histogram behind it. */
|
|
3
4
|
range: (ref, o = {}) => ({
|
|
4
5
|
kind: "range",
|
|
5
6
|
field: ref.name,
|
|
6
7
|
histogram: o.histogram ?? false
|
|
7
8
|
}),
|
|
9
|
+
/**
|
|
10
|
+
* A menu of the field's values with counts; `search` adds a search box for long lists. On a relation it searches the
|
|
11
|
+
* referenced records as you type, on a number field it is a range slider.
|
|
12
|
+
*/
|
|
8
13
|
facets: (ref, o = {}) => ({
|
|
9
14
|
kind: "facets",
|
|
10
15
|
field: ref.name,
|
|
11
16
|
search: o.search ?? false
|
|
12
17
|
}),
|
|
18
|
+
/** A date range with buttons for the `presets`, such as `today` and `30d`. */
|
|
13
19
|
dateRange: (ref, o = {}) => ({
|
|
14
20
|
kind: "dateRange",
|
|
15
21
|
field: ref.name,
|
|
16
22
|
presets: o.presets ?? []
|
|
17
23
|
}),
|
|
24
|
+
/** A switch for a boolean field. */
|
|
18
25
|
toggle: (ref) => ({
|
|
19
26
|
kind: "toggle",
|
|
20
27
|
field: ref.name
|
package/dist/view.d.ts
CHANGED
|
@@ -8,7 +8,11 @@ import { widgets } from "./view-widgets.js";
|
|
|
8
8
|
type FieldsOf<R extends {
|
|
9
9
|
readonly $fields: unknown;
|
|
10
10
|
}> = R['$fields'];
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* `nav(...)` with `recent.status` limited to the resource's fields.
|
|
13
|
+
*
|
|
14
|
+
* @inline
|
|
15
|
+
*/
|
|
12
16
|
type NavInput<R extends {
|
|
13
17
|
readonly $fields: unknown;
|
|
14
18
|
}> = Omit<NavModel, 'recent'> & {
|
|
@@ -16,18 +20,31 @@ type NavInput<R extends {
|
|
|
16
20
|
status: keyof FieldsOf<R> & string;
|
|
17
21
|
};
|
|
18
22
|
};
|
|
19
|
-
|
|
23
|
+
/** The `r` of `.fields((r) => ...)`: a field view per field of the resource, to give hints such as `.label()`. */
|
|
24
|
+
export type FieldViews<R extends {
|
|
25
|
+
/** The resource's fields, as `resource(...)` declares them. */
|
|
20
26
|
readonly $fields: unknown;
|
|
21
27
|
}> = { readonly [K in keyof FieldRefs<FieldsOf<R>> & string]: FieldView<K, NonNullable<FieldRefs<FieldsOf<R>>[K]['$value']>>; };
|
|
28
|
+
/**
|
|
29
|
+
* A view of a resource, as `view(...)` starts it: how the admin shows it. Each method returns a new view, so one view
|
|
30
|
+
* can build on another. See [UI config](website/src/content/docs/reference/ui-config.mdx).
|
|
31
|
+
*/
|
|
22
32
|
export declare class ViewBuilder<R extends {
|
|
33
|
+
/** The resource's fields, as `resource(...)` declares them. */
|
|
23
34
|
readonly $fields: unknown;
|
|
24
35
|
}> {
|
|
36
|
+
/** @internal */
|
|
25
37
|
readonly state: ViewModel;
|
|
26
|
-
|
|
38
|
+
/** @internal */
|
|
39
|
+
constructor(
|
|
40
|
+
/** @internal */
|
|
41
|
+
state: ViewModel);
|
|
42
|
+
/** What a record and the records are called, such as "New repair" and "Repairs"; the resource name in words by default. */
|
|
27
43
|
names(names: {
|
|
28
44
|
singular: string;
|
|
29
45
|
plural: string;
|
|
30
46
|
}): ViewBuilder<R>;
|
|
47
|
+
/** An icon name carried in `/meta`, for apps that draw their own shell; the admin picks sidebar icons by resource name. */
|
|
31
48
|
icon(icon: string): ViewBuilder<R>;
|
|
32
49
|
/** Shows this view only to users with one of these roles; a view without roles is the default. */
|
|
33
50
|
forRoles(roles: string[]): ViewBuilder<R>;
|
|
@@ -41,9 +58,11 @@ export declare class ViewBuilder<R extends {
|
|
|
41
58
|
title?: Ref | Ref[];
|
|
42
59
|
subtitle?: Ref;
|
|
43
60
|
}): ViewBuilder<R>;
|
|
44
|
-
/** Where the resource sits in the sidebar; `recent` lists a few of its records under the entry (see
|
|
61
|
+
/** Where the resource sits in the sidebar; `recent` lists a few of its records under the entry (see [Sidebar](website/src/content/docs/reference/sidebar.md#recent-records)). */
|
|
45
62
|
nav(nav: NavInput<R>): ViewBuilder<R>;
|
|
63
|
+
/** A line under the list's title. */
|
|
46
64
|
help(help: string): ViewBuilder<R>;
|
|
65
|
+
/** Hints per field, such as `{ total: r.total.prefix('€').decimals(2) }`; calls add up. */
|
|
47
66
|
fields(build: (r: FieldViews<R>) => Record<string, FieldView>): ViewBuilder<R>;
|
|
48
67
|
/**
|
|
49
68
|
* The list: its table's `columns`, `sort` and `search`, and other `displays` to switch to (`d.cards`, `d.custom`), one
|
|
@@ -56,21 +75,50 @@ export declare class ViewBuilder<R extends {
|
|
|
56
75
|
displays?: ListDisplayModel[];
|
|
57
76
|
default?: string;
|
|
58
77
|
}): ViewBuilder<R>;
|
|
78
|
+
/**
|
|
79
|
+
* The filter widgets above the list, in order, each on a `.filterable()` field: `w.facets`, `w.dateRange`, `w.range`,
|
|
80
|
+
* `w.toggle` and `w.flags`. Together they write the list's `?filter=`.
|
|
81
|
+
*/
|
|
59
82
|
filters(build: (r: FieldRefs<FieldsOf<R>>, w: typeof widgets) => FilterWidgetModel[]): ViewBuilder<R>;
|
|
83
|
+
/**
|
|
84
|
+
* The record page, as items of `l` in order: sections, sidebar cards, related records, maps and custom components.
|
|
85
|
+
* Without it the page shows one section with every field.
|
|
86
|
+
*/
|
|
60
87
|
layout(build: (r: FieldRefs<FieldsOf<R>>) => LayoutItemModel[]): ViewBuilder<R>;
|
|
88
|
+
/** A chart of records per `granularity` over a date or timestamp `field` above the list, following its filters. */
|
|
61
89
|
chart(build: (r: FieldRefs<FieldsOf<R>>) => {
|
|
62
90
|
field: Ref;
|
|
63
91
|
range: ChartRange;
|
|
64
92
|
granularity: ChartGranularity;
|
|
65
93
|
}): ViewBuilder<R>;
|
|
94
|
+
/** Where the record page shows that it saved: a toast (the default) or the Save button. */
|
|
66
95
|
saveFeedback(saveFeedback: SaveFeedback): ViewBuilder<R>;
|
|
96
|
+
/**
|
|
97
|
+
* The named actions of the record page's action rail, which composed pages can put on buttons too: `a.update`,
|
|
98
|
+
* `a.remove` and `a.link` need no code, `a.action` runs a handler the app registers.
|
|
99
|
+
*/
|
|
67
100
|
actions(build: (a: typeof actions) => ActionModel[]): ViewBuilder<R>;
|
|
68
101
|
/** With the resource's model, also verifies the title, search result and list display fields and the sidebar's `recent` group. */
|
|
69
102
|
toModel(resource?: ResourceModel): ViewModel;
|
|
70
103
|
private refs;
|
|
71
104
|
private next;
|
|
72
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Starts the view of a resource: how the admin shows it. Pass the resource's type, so every field reference (`r.status`)
|
|
108
|
+
* is checked against it. See [UI config](website/src/content/docs/reference/ui-config.mdx).
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* import { view } from '@protobase/schema'
|
|
113
|
+
* import type { repairs } from './data'
|
|
114
|
+
*
|
|
115
|
+
* export const repairsView = view<typeof repairs>('repairs')
|
|
116
|
+
* .names({ singular: 'Repair', plural: 'Repairs' })
|
|
117
|
+
* .list((r) => ({ columns: [r.number, r.status, r.booked_on], sort: [[r.booked_on, 'desc']] }))
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
73
120
|
export declare const view: <R extends {
|
|
121
|
+
/** The resource's fields, as `resource(...)` declares them. */
|
|
74
122
|
readonly $fields: unknown;
|
|
75
123
|
}>(resource: string) => ViewBuilder<R>;
|
|
76
124
|
//#endregion
|