@softr/sdk 0.0.68-props.0 → 0.0.69-alpha.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.
Files changed (59) hide show
  1. package/dist/auth/current-user-row.d.ts +20 -0
  2. package/dist/auth/current-user-row.js +22 -0
  3. package/dist/auth/methods/profile.d.ts +26 -0
  4. package/dist/auth/methods/profile.js +52 -0
  5. package/dist/auth/methods/utils.d.ts +1 -1
  6. package/dist/auth/platform.d.ts +1 -0
  7. package/dist/auth/platform.js +20 -33
  8. package/dist/auth/server.d.ts +4 -0
  9. package/dist/auth/server.js +5 -1
  10. package/dist/auth/session.js +12 -2
  11. package/dist/auth/user.d.ts +5 -8
  12. package/dist/auth/user.js +2 -1
  13. package/dist/cli.js +18 -26
  14. package/dist/codegen/datasource-docs.js +1 -93
  15. package/dist/codegen/datasource-types.js +1 -16
  16. package/dist/codegen/fetch-metadata.js +17 -44
  17. package/dist/codegen/softr-env.js +1 -2
  18. package/dist/codegen/softr-fields.js +41 -0
  19. package/dist/cookies.js +2 -10
  20. package/dist/core/options.d.ts +36 -0
  21. package/dist/core/options.js +39 -0
  22. package/dist/core/use-overrides.d.ts +17 -0
  23. package/dist/core/use-overrides.js +30 -0
  24. package/dist/datasource/datasource.d.ts +2 -0
  25. package/dist/datasource/datasource.js +3 -1
  26. package/dist/datasource/fields.js +3 -2
  27. package/dist/datasource/record.d.ts +2 -1
  28. package/dist/default-error-overlay.d.ts +2 -2
  29. package/dist/fields/format.d.ts +45 -0
  30. package/dist/fields/format.js +140 -0
  31. package/dist/formatting.d.ts +4 -4
  32. package/dist/formatting.js +1 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.js +3 -1
  35. package/dist/mcp.d.ts +2 -2
  36. package/dist/mcp.js +2 -2
  37. package/dist/package.js +1 -1
  38. package/dist/pwa.d.ts +2 -2
  39. package/dist/pwa.js +1 -1
  40. package/dist/server.d.ts +2 -1
  41. package/dist/softr-config.schema.js +24 -45
  42. package/dist/visibility/route.js +1 -1
  43. package/dist/vite.js +19 -1
  44. package/package.json +10 -33
  45. package/schema/softr.schema.json +0 -13
  46. package/skills/softr-sdk/SKILL.md +2 -2
  47. package/skills/softr-sdk/auth/SKILL.md +29 -10
  48. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  49. package/skills/softr-sdk/current-user/SKILL.md +5 -24
  50. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  51. package/skills/softr-sdk/formatting/SKILL.md +19 -1
  52. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  53. package/skills/softr-sdk/mcp/SKILL.md +1 -1
  54. package/skills/softr-sdk/pwa/SKILL.md +1 -1
  55. package/skills/softr-sdk/record-mutations/SKILL.md +13 -1
  56. package/skills/softr-sdk/record-queries/SKILL.md +21 -1
  57. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  58. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  59. package/skills/softr-sdk/workflows/SKILL.md +1 -1
@@ -1,4 +1,4 @@
1
- import { assert, panic } from "../assertions.js";
1
+ import { panic } from "../assertions.js";
2
2
  import { fetch } from "../fetch.js";
3
3
 
4
4
  //#region src/codegen/fetch-metadata.ts
@@ -17,54 +17,27 @@ function metadataSourceFromEnv(env = process.env) {
17
17
  token: env.SOFTR_STUDIO_TOKEN
18
18
  };
19
19
  }
20
- async function fetchSoftrEnvInput(source, aliases) {
20
+ async function fetchSoftrEnvInput(source) {
21
21
  const { studioApiUrl, datasourceApiUrl, applicationId, token } = source;
22
22
  const [registered, userGroups] = await Promise.all([apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/datasources`, token), apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/access-control/user-groups`, token)]);
23
- const datasources = await Promise.all(registered.map(async ({ name, datasource }) => {
24
- const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/vibe/studio/applications/${applicationId}/datasources/${datasource.id}/metadata`, token);
25
- return {
26
- ...metadata,
27
- id: datasource.id,
28
- tableId: metadata.id,
29
- name
30
- };
31
- }));
32
- let users = null;
33
- if (Object.keys(aliases).length > 0) {
34
- const { payload } = await apiFetch(`${studioApiUrl}/v1/applications/${applicationId}`, token);
35
- const connection = payload.user_connection?.datasource ?? panic("softr.json declares user.properties, but this app has no users table.");
36
- const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/${applicationId}/${connection.integration_id}/metadata`, token, {
37
- datasourceType: connection.datasource_integration_type,
38
- resourceIdentifier: { keys: connection.resource_identifier.keys }
39
- });
40
- users = {
41
- datasourceId: connection.id,
42
- tableId: metadata.id,
43
- name: metadata.name,
44
- properties: {}
45
- };
46
- for (const [alias, fieldId] of Object.entries(aliases)) {
47
- const field = metadata.fields.find(({ id }) => id === fieldId) ?? panic(`softr.json user.properties.${alias} names "${fieldId}", which is not a field of the users table.`);
48
- assert(field.type !== "ATTACHMENT" && field.type !== "LONG_TEXT" && !(field.allowMultipleEntries && (field.type === "LINKED_RECORD" || field.type === "LOOKUP")), `softr.json user.properties.${alias} names "${field.name}", a ${field.allowMultipleEntries ? "multiple " : ""}${field.type} field, which can be too big to read with the signed-in user. Remove it, and read it with defineQuery on the users table instead.`);
49
- users.properties[alias] = field;
50
- }
51
- }
52
23
  return {
53
- datasources,
54
- userGroups,
55
- users
24
+ datasources: await Promise.all(registered.map(async ({ name, datasource }) => {
25
+ const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/vibe/studio/applications/${applicationId}/datasources/${datasource.id}/metadata`, token);
26
+ return {
27
+ ...metadata,
28
+ id: datasource.id,
29
+ tableId: metadata.id,
30
+ name
31
+ };
32
+ })),
33
+ userGroups
56
34
  };
57
35
  }
58
- async function apiFetch(url, token, body) {
59
- const response = await fetch(url, {
60
- method: body === void 0 ? "GET" : "POST",
61
- headers: {
62
- accept: "application/json",
63
- ...body === void 0 ? {} : { "content-type": "application/json" },
64
- Authorization: `Bearer ${token}`
65
- },
66
- body: body === void 0 ? void 0 : JSON.stringify(body)
67
- }).catch(() => panic(`Could not reach ${url}`));
36
+ async function apiFetch(url, token) {
37
+ const response = await fetch(url, { headers: {
38
+ accept: "application/json",
39
+ Authorization: `Bearer ${token}`
40
+ } }).catch(() => panic(`Could not reach ${url}`));
68
41
  if (!response.ok) panic(`${url} answered ${response.status}`);
69
42
  return await response.json();
70
43
  }
@@ -1,4 +1,4 @@
1
- import { renderDatasourceTypes, renderUserProperties } from "./datasource-types.js";
1
+ import { renderDatasourceTypes } from "./datasource-types.js";
2
2
  import { renderUserGroups } from "./user-groups.js";
3
3
 
4
4
  //#region src/codegen/softr-env.ts
@@ -15,7 +15,6 @@ function renderSoftrEnv(input, sdkVersion) {
15
15
  indent(interfaces),
16
16
  "",
17
17
  indent(renderUserGroups(input.userGroups)),
18
- ...input.users === null ? [] : ["", indent(renderUserProperties(input.users.properties, input.datasources))],
19
18
  "}",
20
19
  ""
21
20
  ].join("\n");
@@ -0,0 +1,41 @@
1
+ //#region src/codegen/softr-fields.ts
2
+ /**
3
+ * Renders the app's `src/softr-fields.ts`: every registered datasource's fields by id, with the type
4
+ * and options a value's text depends on, registered with the SDK as a side effect of being imported.
5
+ * The root route imports it once, and `formatValue(query, alias, value, formatting)` then formats any
6
+ * value a query read by its field's own type.
7
+ */
8
+ function renderSoftrFields(datasources, sdkVersion) {
9
+ const entries = [...datasources].sort((a, b) => a.id.localeCompare(b.id)).map((datasource) => {
10
+ const fields = [...datasource.fields].sort((a, b) => a.id.localeCompare(b.id)).map((field) => {
11
+ const shape = { type: field.type };
12
+ if (field.options !== null && field.options !== void 0 && Object.keys(field.options).length > 0) shape.options = field.options;
13
+ if (field.allowMultipleEntries) shape.allowMultipleEntries = true;
14
+ return ` ${JSON.stringify(field.id)}: ${JSON.stringify(shape)},`;
15
+ });
16
+ return [
17
+ ` // ${datasource.name}`,
18
+ ` ${JSON.stringify(datasource.id)}: {`,
19
+ ...fields,
20
+ " },"
21
+ ].join("\n");
22
+ });
23
+ return [
24
+ `// Generated by @softr/sdk ${sdkVersion}. Do not edit: the SDK's codegen rewrites it after a schema change.`,
25
+ "//",
26
+ "// The type and options of every field, by datasource and field id, for `formatValue` and",
27
+ "// `fieldShape` from @softr/sdk. Imported once, in the root route, it registers them with the SDK.",
28
+ "import { registerSoftrFields } from \"@softr/sdk\";",
29
+ "import type { SoftrFieldRegistry } from \"@softr/sdk\";",
30
+ "",
31
+ "export const softrFields = {",
32
+ ...entries,
33
+ "} as const satisfies SoftrFieldRegistry;",
34
+ "",
35
+ "registerSoftrFields(softrFields);",
36
+ ""
37
+ ].join("\n");
38
+ }
39
+
40
+ //#endregion
41
+ export { renderSoftrFields };
package/dist/cookies.js CHANGED
@@ -1,4 +1,3 @@
1
- import { softAssert } from "./assertions.js";
2
1
  import { getRequestHeader, getResponseHeaders, setResponseHeader } from "@tanstack/react-start/server";
3
2
 
4
3
  //#region src/cookies.ts
@@ -39,16 +38,9 @@ const USER_TIMEZONE_COOKIE = "user-timezone";
39
38
  function readUserTimezone() {
40
39
  return readRequestCookies()[USER_TIMEZONE_COOKIE] ?? null;
41
40
  }
42
- /** What browsers keep of one cookie's name and value. They drop a larger one silently. */
43
- const COOKIE_MAX_BYTES = 4096;
44
41
  function writeCookies(cookies) {
45
- const written = cookies.filter((cookie) => {
46
- const size = cookie.split(";")[0].length;
47
- softAssert(size <= COOKIE_MAX_BYTES, () => `The ${cookie.slice(0, cookie.indexOf("="))} cookie is ${size} bytes, over the ${COOKIE_MAX_BYTES} a browser keeps. It was not written.`);
48
- return size <= COOKIE_MAX_BYTES;
49
- });
50
- if (written.length === 0) return;
51
- setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...written]);
42
+ if (cookies.length === 0) return;
43
+ setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...cookies]);
52
44
  }
53
45
 
54
46
  //#endregion
@@ -0,0 +1,36 @@
1
+ import { DatasourceAction, ReadShape } from "../action.js";
2
+ import "@tanstack/react-start/server-only";
3
+
4
+ //#region src/core/options.d.ts
5
+ /** One row a linked-record picker offers. */
6
+ type RecordOption = {
7
+ id: string;
8
+ label: string;
9
+ };
10
+ /** A page of a picker's rows, in the shape `LinkedRecordPicker`'s `load` answers with. */
11
+ type RecordOptions = {
12
+ options: RecordOption[];
13
+ nextOffset: number | null;
14
+ };
15
+ /**
16
+ * The rows a linked field can point at, for a picker: each record of the query as its id and the text
17
+ * of one field, searched on that field when a term is given, a page at a time.
18
+ *
19
+ * @example
20
+ * const loadCompanies = createServerFn()
21
+ * .validator(z.object({ term: z.string(), offset: z.number().nullable() }))
22
+ * .handler(({ data }) => ds.options(companiesQuery, { label: 'name', ...data }))
23
+ */
24
+ declare function listOptions<TShape extends ReadShape>(query: DatasourceAction<TShape>, {
25
+ label,
26
+ term,
27
+ offset,
28
+ limit
29
+ }: {
30
+ label: keyof TShape["fields"] & string;
31
+ term?: string;
32
+ offset?: number | null;
33
+ limit?: number;
34
+ }): Promise<RecordOptions>;
35
+ //#endregion
36
+ export { RecordOption, RecordOptions, listOptions };
@@ -0,0 +1,39 @@
1
+ import { makeChain } from "../datasource/fields.js";
2
+ import { listRecords } from "../datasource/record.js";
3
+ import "@tanstack/react-start/server-only";
4
+
5
+ //#region src/core/options.ts
6
+ /**
7
+ * The rows a linked field can point at, for a picker: each record of the query as its id and the text
8
+ * of one field, searched on that field when a term is given, a page at a time.
9
+ *
10
+ * @example
11
+ * const loadCompanies = createServerFn()
12
+ * .validator(z.object({ term: z.string(), offset: z.number().nullable() }))
13
+ * .handler(({ data }) => ds.options(companiesQuery, { label: 'name', ...data }))
14
+ */
15
+ async function listOptions(query, { label, term = "", offset = null, limit = 20 }) {
16
+ let list = listRecords(query).limit(limit).offset(offset);
17
+ const needle = term.trim();
18
+ if (needle !== "") list = list.where(() => makeChain(label).contains(needle));
19
+ const page = await list;
20
+ return {
21
+ options: page.records.map((record) => ({
22
+ id: record.id,
23
+ label: labelOf(record.fields[label])
24
+ })),
25
+ nextOffset: page.nextOffset
26
+ };
27
+ }
28
+ /** The text a label field holds: its own text, or the label of the choice or record it names. */
29
+ function labelOf(value) {
30
+ if (value === null || value === void 0) return "";
31
+ if (typeof value === "object") {
32
+ const label = value.label;
33
+ return typeof label === "string" ? label : "";
34
+ }
35
+ return String(value);
36
+ }
37
+
38
+ //#endregion
39
+ export { listOptions };
@@ -0,0 +1,17 @@
1
+ //#region src/core/use-overrides.d.ts
2
+ /**
3
+ * The edits a page has saved that its loader has not re-run for, keyed by record id: what a drawer or
4
+ * an inline control wrote, merged over the loader's rows until a fresh read replaces them.
5
+ *
6
+ * `onSaved` is the shape a drawer's `onSaved` callback takes: it fires before the write, and again
7
+ * with the old value if the write fails, so the row moves at once and moves back on failure.
8
+ */
9
+ declare function useOverrides<TPatch extends object>(): {
10
+ /** What is pending for one record, or undefined when nobody has touched it. */
11
+ of: (recordId: string) => TPatch | undefined;
12
+ onSaved: (recordId: string, patch: TPatch) => void;
13
+ /** Drops what is pending for one record, once a fresh read of it has replaced the row. */
14
+ clear: (recordId: string) => void;
15
+ };
16
+ //#endregion
17
+ export { useOverrides };
@@ -0,0 +1,30 @@
1
+ import { useState } from "react";
2
+
3
+ //#region src/core/use-overrides.ts
4
+ /**
5
+ * The edits a page has saved that its loader has not re-run for, keyed by record id: what a drawer or
6
+ * an inline control wrote, merged over the loader's rows until a fresh read replaces them.
7
+ *
8
+ * `onSaved` is the shape a drawer's `onSaved` callback takes: it fires before the write, and again
9
+ * with the old value if the write fails, so the row moves at once and moves back on failure.
10
+ */
11
+ function useOverrides() {
12
+ const [pending, setPending] = useState({});
13
+ return {
14
+ of: (recordId) => pending[recordId],
15
+ onSaved: (recordId, patch) => setPending((current) => ({
16
+ ...current,
17
+ [recordId]: {
18
+ ...current[recordId],
19
+ ...patch
20
+ }
21
+ })),
22
+ clear: (recordId) => setPending((current) => ({
23
+ ...current,
24
+ [recordId]: void 0
25
+ }))
26
+ };
27
+ }
28
+
29
+ //#endregion
30
+ export { useOverrides };
@@ -1,4 +1,5 @@
1
1
  import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
+ import { listOptions } from "../core/options.js";
2
3
  import { chart, metric } from "./chart.js";
3
4
  import "@tanstack/react-start/server-only";
4
5
 
@@ -11,6 +12,7 @@ declare const ds: {
11
12
  delete: typeof deleteRecord;
12
13
  metric: typeof metric;
13
14
  chart: typeof chart;
15
+ options: typeof listOptions;
14
16
  };
15
17
  //#endregion
16
18
  export { ds };
@@ -1,5 +1,6 @@
1
1
  import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
2
  import { chart, metric } from "./chart.js";
3
+ import { listOptions } from "../core/options.js";
3
4
  import "@tanstack/react-start/server-only";
4
5
 
5
6
  //#region src/datasource/datasource.ts
@@ -10,7 +11,8 @@ const ds = {
10
11
  update: updateRecord,
11
12
  delete: deleteRecord,
12
13
  metric,
13
- chart
14
+ chart,
15
+ options: listOptions
14
16
  };
15
17
 
16
18
  //#endregion
@@ -46,7 +46,8 @@ function group(logicalOperator, filters) {
46
46
  /**
47
47
  * One chain per alias, carrying every operator whatever the alias reads as. Only the types
48
48
  * narrow: a call the types reject cannot reach here, so there is nothing for a per-type
49
- * runtime chain to catch.
49
+ * runtime chain to catch. Exported for a filter built on an alias the caller named, where no
50
+ * type stands behind the chain.
50
51
  */
51
52
  function makeChain(subject) {
52
53
  return {
@@ -87,4 +88,4 @@ function leaf(subject, operator, value) {
87
88
  }
88
89
 
89
90
  //#endregion
90
- export { andAll, makeFieldSet, makeSortSet };
91
+ export { andAll, makeChain, makeFieldSet, makeSortSet };
@@ -68,6 +68,7 @@ interface ListResult<TRecord extends SoftrRecord = SoftrRecord> {
68
68
  total: number;
69
69
  nextOffset: number | null;
70
70
  }
71
+ type GetRecord<TShape extends ReadShape> = SoftrRecord<ActionRecord<TShape>> | null;
71
72
  /**
72
73
  * Queries a list of records from the datasource.
73
74
  *
@@ -135,4 +136,4 @@ declare function deleteRecord<TShape extends ActionShape & {
135
136
  id: string;
136
137
  }): Promise<void>;
137
138
  //#endregion
138
- export { WhereCallback, createRecord, deleteRecord, getRecord, listRecords, updateRecord };
139
+ export { GetRecord, WhereCallback, createRecord, deleteRecord, getRecord, listRecords, updateRecord };
@@ -1,4 +1,4 @@
1
- import * as react6 from "react";
1
+ import * as react4 from "react";
2
2
 
3
3
  //#region src/default-error-overlay.d.ts
4
4
  type Props = {
@@ -8,6 +8,6 @@ type Props = {
8
8
  declare function DefaultErrorOverlay({
9
9
  error,
10
10
  reset
11
- }: Props): react6.JSX.Element;
11
+ }: Props): react4.JSX.Element;
12
12
  //#endregion
13
13
  export { DefaultErrorOverlay };
@@ -0,0 +1,45 @@
1
+ import { Formatting } from "../formatting.js";
2
+
3
+ //#region src/fields/format.d.ts
4
+
5
+ /**
6
+ * A field's type and options, which is all its text depends on. The generated `src/softr-fields.ts`
7
+ * holds one per field of every registered datasource.
8
+ */
9
+ type FieldShape = {
10
+ type: string;
11
+ options?: Record<string, unknown> | null;
12
+ /** True when the field holds several values, which read back as an array. */
13
+ allowMultipleEntries?: boolean;
14
+ };
15
+ /** The fields of every registered datasource, by datasource id then field id. */
16
+ type SoftrFieldRegistry = Record<string, Record<string, FieldShape>>;
17
+ /**
18
+ * Makes the app's fields known to `fieldShape` and `formatValue`. The generated `src/softr-fields.ts`
19
+ * calls it once as a side effect of being imported; the root route imports that file.
20
+ */
21
+ declare function registerSoftrFields(fields: SoftrFieldRegistry): void;
22
+ /**
23
+ * The shape of a field a query reads, by the alias the query holds it under. Throws when the app has
24
+ * not registered its fields: the generated `src/softr-fields.ts` is not imported.
25
+ */
26
+ declare function fieldShape(query: {
27
+ datasourceId: string;
28
+ fields: Record<string, string>;
29
+ }, alias: string): FieldShape;
30
+ /**
31
+ * A value a query read, as the text a page shows it as, by the field's own type and options: money with
32
+ * its symbol, a date in the app's format, a choice or a linked record by its label, a rating out of its
33
+ * scale, a duration in hours and minutes, a checkbox as Yes or No. '' for a value the record does not
34
+ * hold. Several values read joined by a comma.
35
+ */
36
+ declare function formatValue(query: {
37
+ datasourceId: string;
38
+ fields: Record<string, string>;
39
+ }, alias: string, value: unknown, formatting: Formatting): string;
40
+ /** `formatValue` for a shape in hand, such as one from `src/softr-fields.ts` or a `returnedField`. */
41
+ declare function formatField(shape: FieldShape, value: unknown, formatting: Formatting): string;
42
+ /** A duration, which the datasource holds in milliseconds, as hours and minutes; seconds only under an hour. */
43
+ declare function formatDuration(milliseconds: number): string;
44
+ //#endregion
45
+ export { FieldShape, SoftrFieldRegistry, fieldShape, formatDuration, formatField, formatValue, registerSoftrFields };
@@ -0,0 +1,140 @@
1
+ import { formatDate, formatDateTime, formatNumber } from "../formatting.js";
2
+
3
+ //#region src/fields/format.ts
4
+ let registry = {};
5
+ /**
6
+ * Makes the app's fields known to `fieldShape` and `formatValue`. The generated `src/softr-fields.ts`
7
+ * calls it once as a side effect of being imported; the root route imports that file.
8
+ */
9
+ function registerSoftrFields(fields) {
10
+ registry = {
11
+ ...registry,
12
+ ...fields
13
+ };
14
+ }
15
+ /**
16
+ * The shape of a field a query reads, by the alias the query holds it under. Throws when the app has
17
+ * not registered its fields: the generated `src/softr-fields.ts` is not imported.
18
+ */
19
+ function fieldShape(query, alias) {
20
+ const fieldId = query.fields[alias];
21
+ if (fieldId === void 0) throw new Error(`The query has no field "${alias}"; it reads ${Object.keys(query.fields).join(", ")}.`);
22
+ const shape = registry[query.datasourceId]?.[fieldId];
23
+ if (shape === void 0) throw new Error(`No shape is registered for field ${fieldId} of datasource ${query.datasourceId}. Import the generated src/softr-fields.ts once, in the root route, and run the SDK's codegen after a schema change.`);
24
+ return shape;
25
+ }
26
+ /**
27
+ * A value a query read, as the text a page shows it as, by the field's own type and options: money with
28
+ * its symbol, a date in the app's format, a choice or a linked record by its label, a rating out of its
29
+ * scale, a duration in hours and minutes, a checkbox as Yes or No. '' for a value the record does not
30
+ * hold. Several values read joined by a comma.
31
+ */
32
+ function formatValue(query, alias, value, formatting) {
33
+ return formatField(fieldShape(query, alias), value, formatting);
34
+ }
35
+ /** `formatValue` for a shape in hand, such as one from `src/softr-fields.ts` or a `returnedField`. */
36
+ function formatField(shape, value, formatting) {
37
+ if (value === null || value === void 0) return "";
38
+ if (Array.isArray(value)) return value.map((entry) => formatField({
39
+ ...shape,
40
+ allowMultipleEntries: false
41
+ }, entry, formatting)).filter((text$1) => text$1 !== "").join(", ");
42
+ const returned = computedReturns(shape);
43
+ if (returned !== null) return formatField(returned, value, formatting);
44
+ const options = shape.options ?? {};
45
+ switch (shape.type) {
46
+ case "CHECKBOX": return value === true ? "Yes" : "No";
47
+ case "NUMBER":
48
+ case "COUNT":
49
+ case "PROGRESS": return typeof value === "number" ? formatNumber(value, formatting, digits(precisionOf(options))) : text(value);
50
+ case "AUTONUMBER": return text(value);
51
+ case "CURRENCY": {
52
+ if (typeof value !== "number") return text(value);
53
+ const code = options.isoCode;
54
+ const precision = precisionOf(options) ?? 2;
55
+ const cents = {
56
+ minimumFractionDigits: Number.isInteger(value) ? 0 : precision,
57
+ maximumFractionDigits: precision
58
+ };
59
+ if (typeof code === "string" && /^[A-Z]{3}$/.test(code)) return formatNumber(value, formatting, {
60
+ style: "currency",
61
+ currency: code,
62
+ ...cents
63
+ });
64
+ const amount = formatNumber(value, formatting, cents);
65
+ const symbol = typeof options.customSymbol === "string" ? options.customSymbol : "";
66
+ if (symbol === "") return amount;
67
+ return options.symbolOrCodePosition === "AFTER" ? `${amount} ${symbol}` : `${symbol}${amount}`;
68
+ }
69
+ case "PERCENT": return typeof value === "number" ? `${formatNumber(value, formatting, digits(precisionOf(options)))}%` : text(value);
70
+ case "RATING": return typeof value === "number" ? `${value}/${numberOption(options, "max") ?? 5}` : text(value);
71
+ case "DURATION": return typeof value === "number" ? formatDuration(value) : text(value);
72
+ case "DATE":
73
+ case "DATETIME": return isDateValue(value) ? formatDateTime(value, formatting, { dateStyle: "medium" }) : text(value);
74
+ case "CREATED_AT":
75
+ case "UPDATED_AT":
76
+ if (!isDateValue(value)) return text(value);
77
+ return options.precision === "DATE" ? formatDate(value, formatting, { dateStyle: "medium" }) : formatDateTime(value, formatting, { dateStyle: "medium" });
78
+ case "DATE_RANGE": {
79
+ const range = value;
80
+ return [isDateValue(range.from) ? formatDateTime(range.from, formatting, { dateStyle: "medium" }) : "", isDateValue(range.to) ? formatDateTime(range.to, formatting, { dateStyle: "medium" }) : ""].filter((part) => part !== "").join(" – ");
81
+ }
82
+ case "USER": {
83
+ const person = value;
84
+ return text(person.name ?? person.email);
85
+ }
86
+ case "ATTACHMENT": {
87
+ const file = value;
88
+ return typeof file.filename === "string" && file.filename !== "" ? file.filename : "File";
89
+ }
90
+ default:
91
+ if (typeof value === "object" && "label" in value) return text(value.label);
92
+ return text(value);
93
+ }
94
+ }
95
+ /** The shape a FORMULA, LOOKUP or ROLLUP reads as, when its options say; null for any other field. */
96
+ function computedReturns(shape) {
97
+ if (shape.type !== "FORMULA" && shape.type !== "LOOKUP" && shape.type !== "ROLLUP") return null;
98
+ const returned = shape.options?.returnedField;
99
+ if (typeof returned !== "object" || returned === null || !("type" in returned) || typeof returned.type !== "string") return null;
100
+ const options = "options" in returned && typeof returned.options === "object" ? returned.options : null;
101
+ return {
102
+ type: returned.type,
103
+ options
104
+ };
105
+ }
106
+ function text(value) {
107
+ return value === null || value === void 0 ? "" : String(value);
108
+ }
109
+ function isDateValue(value) {
110
+ return typeof value === "string" && value !== "" || typeof value === "number" || value instanceof Date;
111
+ }
112
+ function numberOption(options, key) {
113
+ const value = options[key];
114
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
115
+ }
116
+ function precisionOf(options) {
117
+ return numberOption(options, "precision");
118
+ }
119
+ function digits(precision) {
120
+ return precision === void 0 ? void 0 : {
121
+ minimumFractionDigits: precision,
122
+ maximumFractionDigits: precision
123
+ };
124
+ }
125
+ /** A duration, which the datasource holds in milliseconds, as hours and minutes; seconds only under an hour. */
126
+ function formatDuration(milliseconds) {
127
+ const total = Math.round(milliseconds / 1e3);
128
+ const hours = Math.floor(total / 3600);
129
+ const minutes = Math.floor(total % 3600 / 60);
130
+ const rest = total % 60;
131
+ const parts = [
132
+ hours > 0 ? `${hours}h` : "",
133
+ minutes > 0 ? `${minutes}m` : "",
134
+ hours === 0 && rest > 0 ? `${rest}s` : ""
135
+ ].filter((part) => part !== "");
136
+ return parts.length === 0 ? "0m" : parts.join(" ");
137
+ }
138
+
139
+ //#endregion
140
+ export { fieldShape, formatDuration, formatField, formatValue, registerSoftrFields };
@@ -1,6 +1,6 @@
1
- import { z } from "zod";
2
- import * as react2 from "react";
1
+ import * as react0 from "react";
3
2
  import { ReactNode } from "react";
3
+ import { z } from "zod";
4
4
 
5
5
  //#region src/formatting.d.ts
6
6
  type FixedDateFormat = "FRIENDLY" | "DD-MM-YYYY" | "MM-DD-YYYY" | "YYYY-MM-DD";
@@ -94,7 +94,7 @@ declare function FormattingScope({
94
94
  /** A BCP 47 locale, such as `de-DE`. It drops the app's fixed date format inside the scope. */
95
95
  locale?: string;
96
96
  children?: ReactNode;
97
- }): react2.FunctionComponentElement<react2.ProviderProps<Scope>>;
97
+ }): react0.FunctionComponentElement<react0.ProviderProps<Scope>>;
98
98
  /**
99
99
  * A date in the app's format, safe to render on the server. A timestamp whose
100
100
  * zone is not known yet renders `fallback` until the browser supplies it, so
@@ -115,6 +115,6 @@ declare function FormattedDate({
115
115
  time?: boolean;
116
116
  /** What renders while the visitor's zone is unknown. */
117
117
  fallback?: ReactNode;
118
- }): react2.FunctionComponentElement<react2.FragmentProps>;
118
+ }): react0.FunctionComponentElement<react0.FragmentProps>;
119
119
  //#endregion
120
120
  export { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting };
@@ -1,6 +1,6 @@
1
1
  import { logger } from "./logger.js";
2
- import { z } from "zod";
3
2
  import { Fragment, createContext, createElement, useContext, useMemo, useSyncExternalStore } from "react";
3
+ import { z } from "zod";
4
4
  import { rootRouteId, useRouteContext } from "@tanstack/react-router";
5
5
 
6
6
  //#region src/formatting.ts
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SoftrUserGroupId, SoftrUserGroups, SoftrUserProperties, User } from "./auth/user.js";
1
+ import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
2
2
  import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
3
3
  import { FileAttachment } from "./core/file-upload.js";
4
4
  import { FileItem, useFileUpload } from "./core/use-file-upload.js";
@@ -10,8 +10,10 @@ import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedErr
10
10
  import { toReturnPath } from "./auth/returnPath.js";
11
11
  import { AuthOptions, AuthSettingsProvider, getAuthSettings, getSoftrContext, initialSoftrContext, softrAuthMiddleware, useAuthSettings, useCurrentUser } from "./auth/isomorphic.js";
12
12
  import { softrMcpMiddleware } from "./mcp.js";
13
+ import { FieldShape, SoftrFieldRegistry, fieldShape, formatDuration, formatField, formatValue, registerSoftrFields } from "./fields/format.js";
14
+ import { useOverrides } from "./core/use-overrides.js";
13
15
  import { useUserTimezone } from "./timezone.js";
14
16
  import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
15
17
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
16
18
  import { Audience } from "./visibility/audience.js";
17
- export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, formatDate, formatDateTime, formatNumber, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, usePwaInstall, useUserTimezone };
19
+ export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
package/dist/index.js CHANGED
@@ -7,8 +7,10 @@ import { FormattedDate, FormattingScope, formatDate, formatDateTime, formatNumbe
7
7
  import { SoftrPwaHead, usePwaInstall } from "./pwa.js";
8
8
  import { AuthSettingsProvider, getAuthSettings, getSoftrContext, initialSoftrContext, softrAuthMiddleware, useAuthSettings, useCurrentUser } from "./auth/isomorphic.js";
9
9
  import { softrMcpMiddleware } from "./mcp.js";
10
+ import { fieldShape, formatDuration, formatField, formatValue, registerSoftrFields } from "./fields/format.js";
11
+ import { useOverrides } from "./core/use-overrides.js";
10
12
  import { useUserTimezone } from "./timezone.js";
11
13
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
12
14
  import { Audience } from "./visibility/audience.js";
13
15
 
14
- export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, formatDate, formatDateTime, formatNumber, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, usePwaInstall, useUserTimezone };
16
+ export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DatasourceAction } from "./action.js";
2
- import * as _tanstack_react_start4 from "@tanstack/react-start";
2
+ import * as _tanstack_react_start0 from "@tanstack/react-start";
3
3
 
4
4
  //#region src/mcp.d.ts
5
5
 
@@ -31,6 +31,6 @@ type SoftrMcpOptions = {
31
31
  * ],
32
32
  * }))
33
33
  */
34
- declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start4.RequestMiddlewareAfterServer<{}, undefined, undefined>;
34
+ declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>;
35
35
  //#endregion
36
36
  export { softrMcpMiddleware };
package/dist/mcp.js CHANGED
@@ -2,12 +2,12 @@ import { assert } from "./assertions.js";
2
2
  import { fetch } from "./fetch.js";
3
3
  import { sessionConfig } from "./auth/config.js";
4
4
  import { readSessionToken, withSessionToken } from "./auth/session.js";
5
- import { softrConfig } from "./softr-config.js";
6
5
  import { makePlatform, platformApiUrl } from "./auth/platform.js";
7
6
  import { isDatasourceAction } from "./action.js";
7
+ import { softrConfig } from "./softr-config.js";
8
8
  import { ds } from "./datasource/datasource.js";
9
- import { z } from "zod";
10
9
  import { createMiddleware } from "@tanstack/react-start";
10
+ import { z } from "zod";
11
11
 
12
12
  //#region src/mcp.ts
13
13
  const MCP_PATH = "/_softr/mcp";