@softr/sdk 0.0.68-props.0 → 0.0.68
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/auth/current-user-row.d.ts +20 -0
- package/dist/auth/current-user-row.js +22 -0
- package/dist/auth/platform.d.ts +1 -0
- package/dist/auth/platform.js +13 -33
- package/dist/auth/server.d.ts +2 -0
- package/dist/auth/server.js +3 -1
- package/dist/auth/session.js +1 -1
- package/dist/auth/user.d.ts +1 -8
- package/dist/auth/user.js +1 -2
- package/dist/cli.js +18 -26
- package/dist/codegen/datasource-docs.js +1 -93
- package/dist/codegen/datasource-types.js +1 -16
- package/dist/codegen/fetch-metadata.js +17 -44
- package/dist/codegen/softr-env.js +1 -2
- package/dist/codegen/softr-fields.js +41 -0
- package/dist/cookies.js +2 -10
- package/dist/core/options.d.ts +36 -0
- package/dist/core/options.js +39 -0
- package/dist/core/use-overrides.d.ts +17 -0
- package/dist/core/use-overrides.js +30 -0
- package/dist/datasource/datasource.d.ts +2 -0
- package/dist/datasource/datasource.js +3 -1
- package/dist/datasource/fields.js +3 -2
- package/dist/datasource/record.d.ts +2 -1
- package/dist/fields/format.d.ts +45 -0
- package/dist/fields/format.js +140 -0
- package/dist/formatting.d.ts +4 -4
- package/dist/formatting.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/mcp.js +2 -2
- package/dist/package.js +1 -1
- package/dist/pwa.d.ts +2 -2
- package/dist/pwa.js +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/softr-config.schema.js +24 -45
- package/dist/visibility/route.js +1 -1
- package/dist/vite.js +19 -1
- package/package.json +10 -33
- package/schema/softr.schema.json +0 -13
- package/skills/softr-sdk/SKILL.md +2 -2
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +3 -24
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +19 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +13 -1
- package/skills/softr-sdk/record-queries/SKILL.md +21 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
|
@@ -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 };
|
|
@@ -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 };
|
package/dist/formatting.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
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
|
-
}):
|
|
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
|
-
}):
|
|
118
|
+
}): react0.FunctionComponentElement<react0.FragmentProps>;
|
|
119
119
|
//#endregion
|
|
120
120
|
export { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting };
|
package/dist/formatting.js
CHANGED
|
@@ -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,
|
|
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
|
|
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.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";
|
package/dist/package.js
CHANGED
package/dist/pwa.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react4 from "react";
|
|
2
2
|
import "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/pwa.d.ts
|
|
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
|
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
25
|
*/
|
|
26
|
-
declare function SoftrPwaHead():
|
|
26
|
+
declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
|
|
27
27
|
type PwaPlatform = "ios" | "android" | "desktop";
|
|
28
28
|
type PwaInstall = {
|
|
29
29
|
available: false;
|
package/dist/pwa.js
CHANGED
|
@@ -2,9 +2,9 @@ import { logger } from "./logger.js";
|
|
|
2
2
|
import { fetch } from "./fetch.js";
|
|
3
3
|
import { sessionConfig } from "./auth/config.js";
|
|
4
4
|
import { platformApiUrl } from "./auth/platform.js";
|
|
5
|
-
import { z } from "zod";
|
|
6
5
|
import { Fragment, createElement, useEffect, useState } from "react";
|
|
7
6
|
import { createMiddleware } from "@tanstack/react-start";
|
|
7
|
+
import { z } from "zod";
|
|
8
8
|
|
|
9
9
|
//#region src/pwa.ts
|
|
10
10
|
const MANIFEST_PATH = "/manifest.webmanifest";
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { upload } from "./core/file-upload.js";
|
|
2
2
|
import { auth } from "./auth/server.js";
|
|
3
|
+
import { RecordOption, RecordOptions } from "./core/options.js";
|
|
3
4
|
import { ds } from "./datasource/datasource.js";
|
|
4
5
|
import { workflow } from "./core/workflow.js";
|
|
5
6
|
import { integration } from "./core/integration.js";
|
|
6
7
|
import "@tanstack/react-start/server-only";
|
|
7
|
-
export { auth, ds, integration, upload, workflow };
|
|
8
|
+
export { type RecordOption, type RecordOptions, auth, ds, integration, upload, workflow };
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { assert, panic } from "./assertions.js";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
1
|
import { z } from "zod";
|
|
5
2
|
|
|
6
3
|
//#region src/softr-config.schema.ts
|
|
7
|
-
const SOFTR_CONFIG_FILE = "softr.json";
|
|
8
4
|
const AccessBehaviour = z.union([
|
|
9
5
|
z.string(),
|
|
10
6
|
z.object({
|
|
@@ -13,49 +9,32 @@ const AccessBehaviour = z.union([
|
|
|
13
9
|
}),
|
|
14
10
|
z.object({ strategy: z.literal("throw") })
|
|
15
11
|
], { error: "must be a path like \"/login\", { \"strategy\": \"redirect\", \"to\": \"/login\" } or { \"strategy\": \"throw\" }" });
|
|
16
|
-
const AppPath = z.string().refine((path
|
|
17
|
-
const SoftrConfigSchema = z.object({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
redirectParam: z.string()
|
|
23
|
-
}),
|
|
24
|
-
resetPassword: z.strictObject({
|
|
25
|
-
path: AppPath,
|
|
26
|
-
tokenParam: z.string()
|
|
27
|
-
}),
|
|
28
|
-
acceptInvitation: z.strictObject({
|
|
29
|
-
path: AppPath,
|
|
30
|
-
tokenParam: z.string()
|
|
31
|
-
}),
|
|
32
|
-
magicLink: z.strictObject({
|
|
33
|
-
path: AppPath,
|
|
34
|
-
tokenParam: z.string()
|
|
35
|
-
})
|
|
12
|
+
const AppPath = z.string().refine((path) => path.startsWith("/") && !path.includes("?") && !path.includes("#") && new URL(path, "https://app.invalid").origin === "https://app.invalid", { error: "must be a path on this app like \"/login\", with no \"?\" or \"#\"" });
|
|
13
|
+
const SoftrConfigSchema = z.object({ auth: z.strictObject({
|
|
14
|
+
routes: z.strictObject({
|
|
15
|
+
login: z.strictObject({
|
|
16
|
+
path: AppPath,
|
|
17
|
+
redirectParam: z.string()
|
|
36
18
|
}),
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
resetPassword: z.strictObject({
|
|
20
|
+
path: AppPath,
|
|
21
|
+
tokenParam: z.string()
|
|
22
|
+
}),
|
|
23
|
+
acceptInvitation: z.strictObject({
|
|
24
|
+
path: AppPath,
|
|
25
|
+
tokenParam: z.string()
|
|
26
|
+
}),
|
|
27
|
+
magicLink: z.strictObject({
|
|
28
|
+
path: AppPath,
|
|
29
|
+
tokenParam: z.string()
|
|
41
30
|
})
|
|
42
31
|
}),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let parsed;
|
|
50
|
-
try {
|
|
51
|
-
parsed = JSON.parse(source);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
panic(`${SOFTR_CONFIG_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
54
|
-
}
|
|
55
|
-
const result = SoftrConfigSchema.safeParse(parsed);
|
|
56
|
-
if (!result.success) panic(`${SOFTR_CONFIG_FILE} does not match the schema:\n\n${z.prettifyError(result.error)}`);
|
|
57
|
-
return result.data;
|
|
58
|
-
}
|
|
32
|
+
guard: z.strictObject({
|
|
33
|
+
unauthenticated: AccessBehaviour,
|
|
34
|
+
forbidden: AccessBehaviour,
|
|
35
|
+
alreadyAuthenticated: AccessBehaviour
|
|
36
|
+
})
|
|
37
|
+
}) });
|
|
59
38
|
|
|
60
39
|
//#endregion
|
|
61
|
-
export {
|
|
40
|
+
export { SoftrConfigSchema };
|
package/dist/visibility/route.js
CHANGED
package/dist/vite.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assert, panic } from "./assertions.js";
|
|
2
|
+
import { SoftrConfigSchema } from "./softr-config.schema.js";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
4
|
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
3
6
|
import { pathToFileURL } from "node:url";
|
|
4
7
|
|
|
5
8
|
//#region src/vite.ts
|
|
9
|
+
const SOFTR_CONFIG_FILE = "softr.json";
|
|
6
10
|
function softr() {
|
|
7
11
|
return [softrConfig(), editEngine()];
|
|
8
12
|
}
|
|
@@ -23,6 +27,20 @@ function softrConfig() {
|
|
|
23
27
|
}
|
|
24
28
|
};
|
|
25
29
|
}
|
|
30
|
+
function loadSoftrConfig(root) {
|
|
31
|
+
const file = path.join(root, SOFTR_CONFIG_FILE);
|
|
32
|
+
assert(existsSync(file), `${SOFTR_CONFIG_FILE} is missing from ${root}`);
|
|
33
|
+
const source = readFileSync(file, "utf8");
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(source);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
panic(`${SOFTR_CONFIG_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
39
|
+
}
|
|
40
|
+
const result = SoftrConfigSchema.safeParse(parsed);
|
|
41
|
+
if (!result.success) panic(`${SOFTR_CONFIG_FILE} does not match the schema:\n\n${z.prettifyError(result.error)}`);
|
|
42
|
+
return result.data;
|
|
43
|
+
}
|
|
26
44
|
/**
|
|
27
45
|
* The edit engine is provided by the pod runtime rather than an app dependency: it is never in the
|
|
28
46
|
* app's `package.json` and never in its `node_modules`. The pod points `SOFTR_EDIT_ENGINE_DIR` at
|