@protobase/schema 0.13.4 → 0.14.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 (57) hide show
  1. package/dist/access/capability.d.ts +3 -0
  2. package/dist/access/index.d.ts +3 -3
  3. package/dist/access/resolve.d.ts +13 -0
  4. package/dist/access/role-entry.d.ts +5 -0
  5. package/dist/access/roles.d.ts +14 -0
  6. package/dist/access/roles.js +11 -0
  7. package/dist/access/rule.d.ts +10 -0
  8. package/dist/access/rule.js +10 -0
  9. package/dist/access/types.d.ts +11 -0
  10. package/dist/assistant/protocol.d.ts +67 -2
  11. package/dist/bottom-bar-errors.d.ts +3 -0
  12. package/dist/bottom-bar.d.ts +3 -1
  13. package/dist/bottom-bar.js +2 -1
  14. package/dist/events/event-helpers.d.ts +41 -8
  15. package/dist/events/event-helpers.js +1 -3
  16. package/dist/field-ref.d.ts +1 -0
  17. package/dist/field.d.ts +54 -1
  18. package/dist/field.js +16 -0
  19. package/dist/fields.d.ts +31 -0
  20. package/dist/fields.js +31 -0
  21. package/dist/file-field.d.ts +6 -1
  22. package/dist/file-field.js +6 -1
  23. package/dist/files/file-options.d.ts +17 -0
  24. package/dist/filter/check.d.ts +5 -0
  25. package/dist/filter/evaluate.d.ts +1 -0
  26. package/dist/filter/order-by.d.ts +14 -0
  27. package/dist/filter/order-by.js +2 -0
  28. package/dist/filter/parse.d.ts +16 -2
  29. package/dist/filter/parse.js +8 -2
  30. package/dist/filter/where.d.ts +61 -0
  31. package/dist/filter/where.js +34 -0
  32. package/dist/index.d.ts +16 -24
  33. package/dist/index.js +6 -18
  34. package/dist/internal.d.ts +17 -0
  35. package/dist/internal.js +17 -0
  36. package/dist/keys.d.ts +1 -0
  37. package/dist/layout.d.ts +15 -0
  38. package/dist/layout.js +15 -0
  39. package/dist/model.d.ts +344 -6
  40. package/dist/refs.d.ts +6 -0
  41. package/dist/relation-field.d.ts +5 -0
  42. package/dist/relation-field.js +5 -0
  43. package/dist/resource-types.d.ts +18 -0
  44. package/dist/resource.d.ts +94 -6
  45. package/dist/resource.js +75 -2
  46. package/dist/user-menu.d.ts +5 -1
  47. package/dist/user-menu.js +4 -1
  48. package/dist/view-actions.d.ts +1 -0
  49. package/dist/view-displays.d.ts +3 -0
  50. package/dist/view-field.d.ts +14 -1
  51. package/dist/view-field.js +7 -0
  52. package/dist/view-widgets.d.ts +7 -0
  53. package/dist/view-widgets.js +7 -0
  54. package/dist/view.d.ts +52 -4
  55. package/dist/view.js +39 -2
  56. package/dist/webhooks/webhook-resources.js +1 -1
  57. 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
@@ -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
@@ -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,
@@ -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
@@ -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
- type SearchRef = Ref & {
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
- type Validator<F> = (record: RecordOf<F>) => Issue<F>[] | undefined;
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
- constructor(state: ResourceState, validators?: readonly Validator<F>[], accessRules?: Access<F>);
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
- * `(e, r) => [...e.crud(), e.becomes('large', 'total > 1000')]`.
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
- * `(e, r) => [...e.crud(), e.becomes('large', 'total > 1000')]`.
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 };
@@ -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
- /** The pages under the signed-in user, at the bottom of the sidebar, above the built-in "Sign out". */
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
- /** The pages under the signed-in user, at the bottom of the sidebar, above the built-in "Sign out". */
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) };
@@ -1,5 +1,6 @@
1
1
  import { ActionModel, AppAddress } from "./model.js";
2
2
  //#region src/view-actions.d.ts
3
+ /** @inline */
3
4
  type ActionOptions = {
4
5
  label: string;
5
6
  icon?: string;
@@ -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;
@@ -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
- constructor(name: N, props?: FieldViewModel);
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>;
@@ -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
  }
@@ -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
@@ -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
- /** `nav(...)` with `recent.status` limited to the resource's fields. */
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
- type FieldViews<R extends {
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
- constructor(state: ViewModel);
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 https://docs.protobase.net/reference/sidebar/). */
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