@protobase/schema 0.55.3 → 0.55.4

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/model.d.ts CHANGED
@@ -149,6 +149,11 @@ export type ResourceModel = {
149
149
  comments?: true;
150
150
  /** Whether open pages refresh as soon as its records change (`.live()`). */
151
151
  live?: true;
152
+ /**
153
+ * The fields of a unique key whose conflict on create answers the record already holding those values, to a caller
154
+ * who may read it (`.openExisting()`).
155
+ */
156
+ openExisting?: string[];
152
157
  };
153
158
  /** What happened to a record: `delete` and `undelete` include setting and clearing a soft delete field. */
154
159
  export type EventOperation = 'create' | 'update' | 'delete' | 'undelete';
@@ -1118,6 +1123,11 @@ export type NavRecentModel = {
1118
1123
  orderBy?: string;
1119
1124
  /** How many records to show, 1 to 20; 3 when omitted. */
1120
1125
  limit?: number;
1126
+ /**
1127
+ * Marks each record for the person reading, such as bold with a count of unread messages: the name of a hook in the
1128
+ * project UI's `marks`, which the sidebar calls in the browser. Presentation only: it is no field, filter or sort key.
1129
+ */
1130
+ marks?: string;
1121
1131
  };
1122
1132
  /** Where a resource sits in the sidebar (`.nav(...)`); see [Sidebar](website/src/content/docs/reference/sidebar.md). */
1123
1133
  export type NavModel = {
@@ -32,7 +32,8 @@ const navRecentErrors = (recent, model) => [
32
32
  ...Object.entries(recent.tones ?? {}).filter(([, tone]) => !statusTones.includes(tone)).map(([value, tone]) => `tone "${tone}" for "${value}" is not one of ${statusTones.join(", ")}`),
33
33
  ...recent.limit === void 0 || Number.isInteger(recent.limit) && recent.limit >= 1 && recent.limit <= maxLimit ? [] : [`limit must be a whole number from 1 to ${maxLimit}`],
34
34
  ...recent.filter === void 0 ? [] : filterErrors(model, recent.filter).map((error) => `filter: ${error.message}`),
35
- ...recent.orderBy === void 0 ? [] : orderByErrors(model, recent.orderBy).map((error) => `orderBy: ${error.message}`)
35
+ ...recent.orderBy === void 0 ? [] : orderByErrors(model, recent.orderBy).map((error) => `orderBy: ${error.message}`),
36
+ ...recent.marks === void 0 || typeof recent.marks === "string" && recent.marks !== "" ? [] : ["marks needs the name of a hook in the project UI's marks"]
36
37
  ];
37
38
  //#endregion
38
39
  export { defaultRecentLimit, navRecentErrors, statusTones };
@@ -0,0 +1,16 @@
1
+ //#region src/open-existing-problems.ts
2
+ const openExistingProblems = (model) => {
3
+ const names = model.openExisting;
4
+ if (!names) return [];
5
+ if (names.length === 0) return ["openExisting names no field"];
6
+ return names.flatMap((name, index) => {
7
+ const field = model.fields[name];
8
+ return [
9
+ ...field.encrypted ? ["is encrypted"] : [],
10
+ ...field.file ? ["is a file field"] : [],
11
+ ...names.indexOf(name) !== index ? ["is given twice"] : []
12
+ ].map((problem) => `openExisting field "${name}" ${problem}`);
13
+ });
14
+ };
15
+ //#endregion
16
+ export { openExistingProblems };
@@ -24,6 +24,7 @@ export type ResourceState = {
24
24
  history?: boolean;
25
25
  comments?: boolean;
26
26
  live?: boolean;
27
+ openExisting?: string[];
27
28
  };
28
29
  export declare const parseTable: (table: string) => {
29
30
  name: string;
@@ -1,6 +1,7 @@
1
1
  import { promptFields } from "./ai-prompt.js";
2
2
  import { buildEventModels } from "./events/event-models.js";
3
3
  import { computedProblems } from "./computed/computed-problems.js";
4
+ import { openExistingProblems } from "./open-existing-problems.js";
4
5
  import { snakeCase } from "./naming.js";
5
6
  //#region src/resource-model.ts
6
7
  const parseTable = (table) => {
@@ -75,7 +76,8 @@ const assembleModel = (state) => {
75
76
  ...state.comments && { comments: true },
76
77
  ...state.live && { live: true },
77
78
  ...state.position && { position: { ...state.position } },
78
- ...state.parents && { parents: state.parents.map((parent) => ({ ...parent })) }
79
+ ...state.parents && { parents: state.parents.map((parent) => ({ ...parent })) },
80
+ ...state.openExisting && { openExisting: [...state.openExisting] }
79
81
  };
80
82
  };
81
83
  const sensitiveProblems = (model) => Object.values(model.fields).filter((field) => field.sensitive).flatMap((field) => [
@@ -168,6 +170,7 @@ const buildResourceModel = (state) => {
168
170
  ...positionProblems(model),
169
171
  ...parentProblems(model),
170
172
  ...aiProblems(model),
173
+ ...openExistingProblems(model),
171
174
  ...computedProblems(model),
172
175
  ...events?.problems ?? []
173
176
  ];
@@ -82,6 +82,18 @@ export declare class ResourceBuilder<F extends FieldsInput = {}, K = never> {
82
82
  search(pick: (r: SearchRefs<F>) => SearchRef[]): ResourceBuilder<F, K>;
83
83
  /** The field holding the owning user's id, which `.own` capability scopes filter on. */
84
84
  owner(pick: (r: FieldRefs<F>) => Ref): ResourceBuilder<F, K>;
85
+ /**
86
+ * A unique key of the table, as its fields, whose conflict on create opens the record already holding those values:
87
+ * the API's 409 names that record's key (`existing.key`) to a caller who may read it, and the create form opens it.
88
+ * The fields must be exactly the columns of a unique index or constraint, such as one over a generated column. See
89
+ * [Opening an existing record](website/src/content/docs/reference/data-config.mdx#opening-an-existing-record).
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * .openExisting((r) => [r.organization_id, r.member_key])
94
+ * ```
95
+ */
96
+ openExisting(pick: (r: FieldRefs<F>) => Ref[]): ResourceBuilder<F, K>;
85
97
  /**
86
98
  * The field holding the id of the user who created the record, such as a chat message's sender: every create sets it
87
99
  * to the signed-in user's id, and no request may write it, so it is never the client's word. A create by no user
package/dist/resource.js CHANGED
@@ -84,6 +84,20 @@ var ResourceBuilder = class ResourceBuilder {
84
84
  return this.next({ owner: pick(this.refs()).name });
85
85
  }
86
86
  /**
87
+ * A unique key of the table, as its fields, whose conflict on create opens the record already holding those values:
88
+ * the API's 409 names that record's key (`existing.key`) to a caller who may read it, and the create form opens it.
89
+ * The fields must be exactly the columns of a unique index or constraint, such as one over a generated column. See
90
+ * [Opening an existing record](website/src/content/docs/reference/data-config.mdx#opening-an-existing-record).
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * .openExisting((r) => [r.organization_id, r.member_key])
95
+ * ```
96
+ */
97
+ openExisting(pick) {
98
+ return this.next({ openExisting: pick(this.refs()).map((ref) => ref.name) });
99
+ }
100
+ /**
87
101
  * The field holding the id of the user who created the record, such as a chat message's sender: every create sets it
88
102
  * to the signed-in user's id, and no request may write it, so it is never the client's word. A create by no user
89
103
  * leaves it empty.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.55.3",
3
+ "version": "0.55.4",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {