@protobase/server 0.63.4 → 0.64.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.
@@ -10,7 +10,10 @@ const batchApproval = ({ text, described, operations, before, softDeletes }) =>
10
10
  }]] : []));
11
11
  const refName = ({ $ref }) => {
12
12
  const created = creates.get($ref);
13
- return created ? text.fromWrite(resourceLabel(described(created.resource)), created.index + 1) : `"${shown($ref)}"`;
13
+ return created ? text("fromWrite", {
14
+ resource: resourceLabel(described(created.resource)),
15
+ write: created.index + 1
16
+ }) : `"${shown($ref)}"`;
14
17
  };
15
18
  const dataOf = (data) => Object.fromEntries(Object.entries(data).map(([name, value]) => [name, isRef(value) ? refName(value) : value]));
16
19
  const nameOf = (operation, index) => {
@@ -24,34 +27,34 @@ const batchApproval = ({ text, described, operations, before, softDeletes }) =>
24
27
  const { action, fields } = createChange(text, resource, dataOf(operation.data));
25
28
  return {
26
29
  label: label(action),
27
- value: joined(fields, text.noFields)
30
+ value: joined(fields, text("noFields"))
28
31
  };
29
32
  }
30
33
  if (operation.op === "update") {
31
34
  const { action, fields } = updateChange(text, resource, nameOf(operation, index), before.get(index) ?? {}, dataOf(operation.data));
32
35
  return {
33
36
  label: label(action),
34
- value: joined(fields, text.noChanges)
37
+ value: joined(fields, text("noChanges"))
35
38
  };
36
39
  }
37
40
  return {
38
- label: label(text.deleteAction(nameOf(operation, index))),
39
- value: softDeletes.has(operation.resource) ? text.restorable : text.permanent
41
+ label: label(text("deleteAction", { record: nameOf(operation, index) })),
42
+ value: softDeletes.has(operation.resource) ? text("restorable") : text("permanent")
40
43
  };
41
44
  };
42
45
  const permanent = operations.filter((operation) => operation.op === "delete" && !softDeletes.has(operation.resource)).length;
43
46
  const rest = operations.length - 50;
44
47
  return {
45
- title: text.batchTitle(operations.length),
48
+ title: text("batchTitle", { count: operations.length }),
46
49
  body: [
47
- ...permanent > 0 ? [text.permanentDeletes(permanent)] : [],
48
- text.allOrNone,
49
- ...rest > 0 ? [text.moreWrites(rest)] : []
50
+ ...permanent > 0 ? [text("permanentDeletes", { count: permanent })] : [],
51
+ text("allOrNone"),
52
+ ...rest > 0 ? [text("moreWrites", { count: rest })] : []
50
53
  ].join("\n"),
51
54
  fields: operations.slice(0, 50).map(entryOf),
52
55
  tone: permanent > 0 ? "danger" : "warning",
53
- approveLabel: text.writeAll,
54
- rejectLabel: text.reject
56
+ approveLabel: text("writeAll"),
57
+ rejectLabel: text("reject")
55
58
  };
56
59
  };
57
60
  //#endregion
@@ -1,61 +1,6 @@
1
- import { defineMessages } from "../locale/messages.js";
1
+ import { messages } from "../locale/messages.js";
2
2
  //#region src/assistant/write-messages.ts
3
3
  /** The texts of the approvals the write tools ask for, which the user reads in their client. */
4
- const writeText = defineMessages({
5
- en: {
6
- createAction: (resource, title) => `Create ${resource}${title === void 0 ? "" : ` "${title}"`}`,
7
- updateAction: (record) => `Update ${record}`,
8
- deleteAction: (record) => `Delete ${record}`,
9
- asked: (action) => `${action}?`,
10
- batchTitle: (count) => `Run ${count} ${count === 1 ? "write" : "writes"} in one transaction?`,
11
- allOrNone: "Either all of them are written or none.",
12
- permanentDeletes: (count) => `${count} ${count === 1 ? "delete cannot" : "deletes cannot"} be undone: those records are removed for good.`,
13
- moreWrites: (count) => `And ${count} more ${count === 1 ? "write" : "writes"}, not listed.`,
14
- fromWrite: (resource, write) => `the ${resource} from write ${write}`,
15
- writeAll: "Write all",
16
- noFields: "No fields.",
17
- commentTitle: (record) => `Comment on ${record}?`,
18
- comment: "Comment",
19
- create: "Create",
20
- update: "Update",
21
- delete: "Delete",
22
- deleteForGood: "Delete for good",
23
- reject: "Cancel",
24
- noChanges: "No field changes.",
25
- restorable: "It can be restored: the record is kept, marked deleted.",
26
- permanent: "This cannot be undone: the record is removed for good.",
27
- empty: "empty",
28
- hidden: "hidden",
29
- yes: "Yes",
30
- no: "No"
31
- },
32
- nl: {
33
- createAction: (resource, title) => `${resource}${title === void 0 ? "" : ` "${title}"`} aanmaken`,
34
- updateAction: (record) => `${record} bijwerken`,
35
- deleteAction: (record) => `${record} verwijderen`,
36
- asked: (action) => `${action}?`,
37
- batchTitle: (count) => `${count} ${count === 1 ? "schrijfactie" : "schrijfacties"} in één transactie uitvoeren?`,
38
- allOrNone: "Ze worden allemaal opgeslagen of geen enkele.",
39
- permanentDeletes: (count) => `${count} ${count === 1 ? "verwijdering kan" : "verwijderingen kunnen"} niet ongedaan worden gemaakt: die records worden definitief verwijderd.`,
40
- moreWrites: (count) => `En nog ${count} ${count === 1 ? "schrijfactie" : "schrijfacties"}, niet vermeld.`,
41
- fromWrite: (resource, write) => `de ${resource} uit schrijfactie ${write}`,
42
- writeAll: "Alles opslaan",
43
- noFields: "Geen velden.",
44
- commentTitle: (record) => `Reageren op ${record}?`,
45
- comment: "Reageren",
46
- create: "Aanmaken",
47
- update: "Bijwerken",
48
- delete: "Verwijderen",
49
- deleteForGood: "Definitief verwijderen",
50
- reject: "Annuleren",
51
- noChanges: "Er verandert geen veld.",
52
- restorable: "Het kan worden teruggezet: het record blijft bewaard, gemarkeerd als verwijderd.",
53
- permanent: "Dit kan niet ongedaan worden gemaakt: het record wordt definitief verwijderd.",
54
- empty: "leeg",
55
- hidden: "verborgen",
56
- yes: "Ja",
57
- no: "Nee"
58
- }
59
- });
4
+ const writeText = messages("assistant/write");
60
5
  //#endregion
61
6
  export { writeText };
@@ -18,9 +18,9 @@ const resourceLabel = ({ name, view }) => shown(view?.names?.singular ?? humaniz
18
18
  const fieldLabel = ({ view }, name) => shown(view?.fields[name]?.label ?? humanize(name));
19
19
  const shownValue = (text, described, name, value) => {
20
20
  const field = described.model?.fields[name];
21
- if (field?.sensitive) return text.hidden;
22
- if (value === null || value === void 0) return text.empty;
23
- if (typeof value === "boolean") return value ? text.yes : text.no;
21
+ if (field?.sensitive) return text("hidden");
22
+ if (value === null || value === void 0) return text("empty");
23
+ if (typeof value === "boolean") return value ? text("yes") : text("no");
24
24
  if (field?.type === "enum" && typeof value === "string") return shown(enumLabel(value, described.view?.fields[name]?.valueLabels));
25
25
  if (typeof value === "string") return shown(value);
26
26
  if (typeof value === "number" || typeof value === "bigint") return String(value);
@@ -38,9 +38,13 @@ const titleOfNew = (described, data) => {
38
38
  return title === "" ? void 0 : title;
39
39
  };
40
40
  const same = (before, after) => JSON.stringify(before ?? null) === JSON.stringify(after ?? null);
41
+ const createAction = (text, resource, title) => title === void 0 ? text("createAction", { resource }) : text("createNamedAction", {
42
+ resource,
43
+ title
44
+ });
41
45
  /** A create as one change: `Create Task "Fix the sign-in lockout"`, and each field the new record is given by its label. */
42
46
  const createChange = (text, described, data) => ({
43
- action: text.createAction(resourceLabel(described), titleOfNew(described, data)),
47
+ action: createAction(text, resourceLabel(described), titleOfNew(described, data)),
44
48
  fields: Object.entries(data).map(([name, value]) => ({
45
49
  label: fieldLabel(described, name),
46
50
  value: shownValue(text, described, name, value)
@@ -60,7 +64,7 @@ const updateChange = (text, described, record, current, data) => {
60
64
  };
61
65
  });
62
66
  return {
63
- action: text.updateAction(record),
67
+ action: text("updateAction", { record }),
64
68
  fields
65
69
  };
66
70
  };
@@ -68,37 +72,37 @@ const updateChange = (text, described, record, current, data) => {
68
72
  const createApproval = (text, described, data) => {
69
73
  const { action, fields } = createChange(text, described, data);
70
74
  return {
71
- title: text.asked(action),
75
+ title: text("asked", { action }),
72
76
  fields,
73
- approveLabel: text.create,
74
- rejectLabel: text.reject
77
+ approveLabel: text("create"),
78
+ rejectLabel: text("reject")
75
79
  };
76
80
  };
77
81
  /** `Update Task #42 "Fix the sign-in lockout"?`, with each field that changes, or saying none does. */
78
82
  const updateApproval = (text, described, key, current, data) => {
79
83
  const { action, fields } = updateChange(text, described, recordName(described, key, current), current, data);
80
84
  return {
81
- title: text.asked(action),
82
- ...fields.length > 0 ? { fields } : { body: text.noChanges },
83
- approveLabel: text.update,
84
- rejectLabel: text.reject
85
+ title: text("asked", { action }),
86
+ ...fields.length > 0 ? { fields } : { body: text("noChanges") },
87
+ approveLabel: text("update"),
88
+ rejectLabel: text("reject")
85
89
  };
86
90
  };
87
91
  /** `Delete Task #42 "Fix the sign-in lockout"?`, saying whether it can be restored (a soft delete) or is gone for good. */
88
92
  const deleteApproval = (text, described, key, current, restorable) => ({
89
- title: text.asked(text.deleteAction(recordName(described, key, current))),
90
- body: restorable ? text.restorable : text.permanent,
93
+ title: text("asked", { action: text("deleteAction", { record: recordName(described, key, current) }) }),
94
+ body: restorable ? text("restorable") : text("permanent"),
91
95
  tone: restorable ? "warning" : "danger",
92
- approveLabel: restorable ? text.delete : text.deleteForGood,
93
- rejectLabel: text.reject
96
+ approveLabel: restorable ? text("delete") : text("deleteForGood"),
97
+ rejectLabel: text("reject")
94
98
  });
95
99
  const hiddenInComment = /[\p{Cf}\p{Zl}\p{Zp}]|[^\P{Cc}\n]/gu;
96
100
  /** `Comment on Task #42 "Fix the sign-in lockout"?`, with the whole comment, as it will be posted. */
97
101
  const commentApproval = (text, described, key, current, comment) => ({
98
- title: text.commentTitle(recordName(described, key, current)),
102
+ title: text("commentTitle", { record: recordName(described, key, current) }),
99
103
  body: comment.trim().replace(hiddenInComment, " "),
100
- approveLabel: text.comment,
101
- rejectLabel: text.reject
104
+ approveLabel: text("comment"),
105
+ rejectLabel: text("reject")
102
106
  });
103
107
  //#endregion
104
108
  export { commentApproval, createApproval, createChange, deleteApproval, describedResource, recordName, resourceLabel, shown, updateApproval, updateChange };
@@ -50,6 +50,13 @@ const valueColorErrors = (view, model) => Object.entries(view.fields).flatMap(([
50
50
  if (field?.type !== "enum") return [`value colours of "${name}" need an enum field`];
51
51
  return Object.keys(hints.valueColors).filter((value) => !field.enumValues?.includes(value)).map((value) => `value colour "${value}" is not a value of "${name}"`);
52
52
  });
53
+ const numberHintErrors = (view, model) => Object.entries(view.fields).flatMap(([name, hints]) => {
54
+ const field = model.fields[name];
55
+ if (!field) return [];
56
+ const currency = hints.currency !== void 0 && field.type !== "decimal" ? [`currency of "${name}" needs a decimal field, not ${field.type}`] : [];
57
+ const signed = hints.format === "signed" && field.type !== "integer" && field.type !== "bigint" ? [`format('signed') of "${name}" needs an integer field, not ${field.type}`] : [];
58
+ return [...currency, ...signed];
59
+ });
53
60
  const textHints = ["multiline", "markdown"];
54
61
  const textFormats = /* @__PURE__ */ new Set([
55
62
  "email",
@@ -181,6 +188,7 @@ const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
181
188
  ...valueLabelErrors(view, model),
182
189
  ...valueColorErrors(view, model),
183
190
  ...textHintErrors(view, model),
191
+ ...numberHintErrors(view, model),
184
192
  ...listEditErrors(view, model),
185
193
  ...widgetErrors(view, model, fieldTypes),
186
194
  ...related,
@@ -0,0 +1,37 @@
1
+ //#region src/locale/en.json
2
+ var en_default = {
3
+ "assistant/write": {
4
+ "createAction": "Create {resource}",
5
+ "updateAction": "Update {record}",
6
+ "deleteAction": "Delete {record}",
7
+ "asked": "{action}?",
8
+ "batchTitle": "Run {count, plural, one {# write} other {# writes}} in one transaction?",
9
+ "allOrNone": "Either all of them are written or none.",
10
+ "permanentDeletes": "{count, plural, one {# delete cannot} other {# deletes cannot}} be undone: those records are removed for good.",
11
+ "moreWrites": "And {count} more {count, plural, one {write} other {writes}}, not listed.",
12
+ "fromWrite": "the {resource} from write {write}",
13
+ "writeAll": "Write all",
14
+ "noFields": "No fields.",
15
+ "commentTitle": "Comment on {record}?",
16
+ "comment": "Comment",
17
+ "create": "Create",
18
+ "update": "Update",
19
+ "delete": "Delete",
20
+ "deleteForGood": "Delete for good",
21
+ "reject": "Cancel",
22
+ "noChanges": "No field changes.",
23
+ "restorable": "It can be restored: the record is kept, marked deleted.",
24
+ "permanent": "This cannot be undone: the record is removed for good.",
25
+ "empty": "empty",
26
+ "hidden": "hidden",
27
+ "yes": "Yes",
28
+ "no": "No",
29
+ "createNamedAction": "Create {resource} \"{title}\""
30
+ },
31
+ "mcp/approval": {
32
+ "cannotAsk": "This client cannot ask you to approve a change, so nothing was written; use a client that supports MCP elicitation.",
33
+ "approve": "Approve"
34
+ }
35
+ };
36
+ //#endregion
37
+ export { en_default as default };
@@ -1,8 +1,30 @@
1
+ import en_default from "./en.js";
2
+ import nl_default from "./nl.js";
3
+ import { IntlMessageFormat } from "intl-messageformat";
1
4
  //#region src/locale/messages.ts
5
+ const catalogues = {
6
+ en: en_default,
7
+ nl: nl_default
8
+ };
9
+ const formatTags = {
10
+ en: "en-GB",
11
+ nl: "nl-NL"
12
+ };
13
+ const compiled = /* @__PURE__ */ new Map();
14
+ const formatOf = (locale, namespace, id) => {
15
+ const key = `${locale}\0${namespace}\0${id}`;
16
+ const cached = compiled.get(key);
17
+ if (cached) return cached;
18
+ const source = catalogues[locale][namespace][id];
19
+ if (source === void 0) throw new Error(`No text ${namespace}.${id} in ${locale}`);
20
+ const format = new IntlMessageFormat(source, formatTags[locale]);
21
+ compiled.set(key, format);
22
+ return format;
23
+ };
2
24
  /**
3
- * An area's built-in texts in every language the screens come in: the lookup gives those of one locale, as the server
4
- * has no single active one. Dutch must translate every English text, which the types check.
25
+ * An area's built-in texts: the lookup gives those of one locale, as the server has no single active one,
26
+ * `text('en')('approve')` or `text(locale)('batchTitle', { count: 3 })`.
5
27
  */
6
- const defineMessages = (catalogue) => (locale) => catalogue[locale];
28
+ const messages = (namespace) => (locale) => (id, values) => formatOf(locale, namespace, id).format(values);
7
29
  //#endregion
8
- export { defineMessages };
30
+ export { catalogues, messages };
@@ -0,0 +1,37 @@
1
+ //#region src/locale/nl.json
2
+ var nl_default = {
3
+ "assistant/write": {
4
+ "createAction": "{resource} aanmaken",
5
+ "updateAction": "{record} bijwerken",
6
+ "deleteAction": "{record} verwijderen",
7
+ "asked": "{action}?",
8
+ "batchTitle": "{count, plural, one {# schrijfactie} other {# schrijfacties}} in één transactie uitvoeren?",
9
+ "allOrNone": "Ze worden allemaal opgeslagen of geen enkele.",
10
+ "permanentDeletes": "{count, plural, one {# verwijdering kan} other {# verwijderingen kunnen}} niet ongedaan worden gemaakt: die records worden definitief verwijderd.",
11
+ "moreWrites": "En nog {count} {count, plural, one {schrijfactie} other {schrijfacties}}, niet vermeld.",
12
+ "fromWrite": "de {resource} uit schrijfactie {write}",
13
+ "writeAll": "Alles opslaan",
14
+ "noFields": "Geen velden.",
15
+ "commentTitle": "Reageren op {record}?",
16
+ "comment": "Reageren",
17
+ "create": "Aanmaken",
18
+ "update": "Bijwerken",
19
+ "delete": "Verwijderen",
20
+ "deleteForGood": "Definitief verwijderen",
21
+ "reject": "Annuleren",
22
+ "noChanges": "Er verandert geen veld.",
23
+ "restorable": "Het kan worden teruggezet: het record blijft bewaard, gemarkeerd als verwijderd.",
24
+ "permanent": "Dit kan niet ongedaan worden gemaakt: het record wordt definitief verwijderd.",
25
+ "empty": "leeg",
26
+ "hidden": "verborgen",
27
+ "yes": "Ja",
28
+ "no": "Nee",
29
+ "createNamedAction": "{resource} \"{title}\" aanmaken"
30
+ },
31
+ "mcp/approval": {
32
+ "cannotAsk": "Deze client kan je niet vragen een wijziging goed te keuren, dus er is niets opgeslagen; gebruik een client die MCP-elicitation ondersteunt.",
33
+ "approve": "Goedkeuren"
34
+ }
35
+ };
36
+ //#endregion
37
+ export { nl_default as default };
@@ -1,4 +1,4 @@
1
- import { defineMessages } from "../locale/messages.js";
1
+ import { messages } from "../locale/messages.js";
2
2
  import { inputRequired, inputResponse } from "@modelcontextprotocol/server";
3
3
  //#region src/mcp/mcp-approval.ts
4
4
  /** Thrown by `approve` over MCP when the request carries no answer yet: the server then asks the client for one. */
@@ -42,20 +42,11 @@ const messageOf = (request) => [
42
42
  request.fields?.map(({ label, value }) => `${label}: ${value}`).join("\n"),
43
43
  request.code
44
44
  ].filter(Boolean).join("\n\n");
45
- const text = defineMessages({
46
- en: {
47
- cannotAsk: "This client cannot ask you to approve a change, so nothing was written; use a client that supports MCP elicitation.",
48
- approve: "Approve"
49
- },
50
- nl: {
51
- cannotAsk: "Deze client kan je niet vragen een wijziging goed te keuren, dus er is niets opgeslagen; gebruik een client die MCP-elicitation ondersteunt.",
52
- approve: "Goedkeuren"
53
- }
54
- });
45
+ const text = messages("mcp/approval");
55
46
  const cannotAsk = (locale) => ({
56
47
  content: [{
57
48
  type: "text",
58
- text: text(locale).cannotAsk
49
+ text: text(locale)("cannotAsk")
59
50
  }],
60
51
  isError: true
61
52
  });
@@ -74,7 +65,7 @@ const approvalAsked = async (request, capabilities, ask, locale = "en") => {
74
65
  type: "object",
75
66
  properties: { approve: {
76
67
  type: "boolean",
77
- title: request.approveLabel ?? text(locale).approve
68
+ title: request.approveLabel ?? text(locale)("approve")
78
69
  } },
79
70
  required: ["approve"]
80
71
  }
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.63.4";
2
+ var version = "0.64.0";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.63.4",
3
+ "version": "0.64.0",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -42,9 +42,9 @@
42
42
  "@better-auth/sso": "^1.7.12",
43
43
  "@livekit/protocol": "1.53.1",
44
44
  "@modelcontextprotocol/server": "^2.3.1",
45
- "@protobase/layout": "0.63.4",
46
- "@protobase/query": "0.63.4",
47
- "@protobase/schema": "0.63.4",
45
+ "@protobase/layout": "0.64.0",
46
+ "@protobase/query": "0.64.0",
47
+ "@protobase/schema": "0.64.0",
48
48
  "@react-email/render": "^2.1.0",
49
49
  "@scalar/hono-api-reference": "^0.12.10",
50
50
  "@sentry/core": "^11.7.0",
@@ -57,6 +57,7 @@
57
57
  "file-type": "^22.1.1",
58
58
  "hono": "^4.13.13",
59
59
  "image-size": "^2.0.4",
60
+ "intl-messageformat": "^12.1.4",
60
61
  "jose": "^6.2.12",
61
62
  "kysely": "^0.29.6",
62
63
  "nodemailer": "^10.0.16",