@softr/sdk 0.0.41-alpha.0 → 0.0.42
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/cli.js +4 -0
- package/dist/codegen/datasource-docs.js +318 -0
- package/dist/codegen/datasource-types.js +22 -5
- package/dist/codegen/doc-file-names.js +31 -0
- package/dist/codegen/softr-env.js +4 -1
- package/dist/codegen/write-datasource-docs.js +24 -0
- package/dist/package.js +1 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +1 -1
- 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 +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +2 -3
- package/skills/softr-sdk/record-queries/references/field-types.md +2 -2
- package/skills/softr-sdk/record-queries/references/filters-and-sorting.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
import { version } from "./package.js";
|
|
3
3
|
import { fetchSoftrEnvInput, metadataSourceFromEnv } from "./codegen/fetch-metadata.js";
|
|
4
4
|
import { renderSoftrEnv } from "./codegen/softr-env.js";
|
|
5
|
+
import { writeDatasourceDocs } from "./codegen/write-datasource-docs.js";
|
|
5
6
|
import { writeFileSync } from "node:fs";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
|
|
8
9
|
//#region src/cli.ts
|
|
9
10
|
const OUTPUT = "softr-env.d.ts";
|
|
11
|
+
const DOCS_DIR = "docs/datasources";
|
|
10
12
|
async function generate() {
|
|
11
13
|
const input = await fetchSoftrEnvInput(metadataSourceFromEnv());
|
|
12
14
|
writeFileSync(path.resolve(process.cwd(), OUTPUT), renderSoftrEnv(input, version), "utf8");
|
|
13
15
|
console.log(`[softr] wrote ${OUTPUT} (${input.datasources.length} datasources)`);
|
|
16
|
+
writeDatasourceDocs(input.datasources, path.resolve(process.cwd(), DOCS_DIR));
|
|
17
|
+
console.log(`[softr] wrote ${DOCS_DIR} (${input.datasources.length} datasources)`);
|
|
14
18
|
}
|
|
15
19
|
const [command] = process.argv.slice(2);
|
|
16
20
|
if (command === "generate") await generate();
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { assert, requireDefined } from "../assertions.js";
|
|
2
|
+
import { choiceEntries, fieldTypeLiteral, isWritable, option, readExample, resolvedShape, scalarExample, underlyingType } from "./datasource-types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/codegen/datasource-docs.ts
|
|
5
|
+
const DOC_NOTE = "> This document is generated automatically when a change is made in the datasource.";
|
|
6
|
+
const INDEX_NOTE = "> These documents are generated automatically when a change is made in a datasource.";
|
|
7
|
+
function renderDatasourceDoc(datasource, context) {
|
|
8
|
+
return [
|
|
9
|
+
`# ${datasourceLabel(datasource)}`,
|
|
10
|
+
"",
|
|
11
|
+
...summary(datasource, context),
|
|
12
|
+
"",
|
|
13
|
+
DOC_NOTE,
|
|
14
|
+
"",
|
|
15
|
+
"## Field reference",
|
|
16
|
+
"",
|
|
17
|
+
...orderedFields(datasource).flatMap((field) => [...renderField(field, datasource, context), ""]),
|
|
18
|
+
...renderExamples(datasource, context.datasources)
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
/** Two trailing spaces are a markdown hard break, which keeps the block one paragraph. */
|
|
22
|
+
function summary(datasource, context) {
|
|
23
|
+
const lines = [`Datasource ID: ${datasource.id}`];
|
|
24
|
+
const related = relatedLinks(datasource, context);
|
|
25
|
+
if (related.length > 0) {
|
|
26
|
+
const label = related.length === 1 ? "datasource" : "datasources";
|
|
27
|
+
lines.push(`Related ${label}: ${related.join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
return lines.map((line, index) => index === lines.length - 1 ? line : `${line} `);
|
|
30
|
+
}
|
|
31
|
+
function relatedLinks(datasource, context) {
|
|
32
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33
|
+
const links = [];
|
|
34
|
+
for (const field of datasource.fields) {
|
|
35
|
+
const linkedId = option(field.options, "linkedTableId");
|
|
36
|
+
if (typeof linkedId !== "string" || seen.has(linkedId)) continue;
|
|
37
|
+
seen.add(linkedId);
|
|
38
|
+
const linked = context.datasources.get(linkedId);
|
|
39
|
+
if (linked === void 0 || linked.id === datasource.id) continue;
|
|
40
|
+
links.push(docLink(linked, context));
|
|
41
|
+
}
|
|
42
|
+
return links;
|
|
43
|
+
}
|
|
44
|
+
function docLink(datasource, context) {
|
|
45
|
+
const file = requireDefined(context.fileNames.get(datasource.id), `No doc file name for datasource ${datasource.id}`);
|
|
46
|
+
return `[${datasourceLabel(datasource)}](./${file})`;
|
|
47
|
+
}
|
|
48
|
+
function renderDatasourceIndex(datasources, context) {
|
|
49
|
+
const heading = [
|
|
50
|
+
"# Datasources",
|
|
51
|
+
"",
|
|
52
|
+
INDEX_NOTE,
|
|
53
|
+
""
|
|
54
|
+
];
|
|
55
|
+
if (datasources.length === 0) return [
|
|
56
|
+
...heading,
|
|
57
|
+
"This app has no registered datasources.",
|
|
58
|
+
""
|
|
59
|
+
].join("\n");
|
|
60
|
+
return [
|
|
61
|
+
...heading,
|
|
62
|
+
"Datasources available in this app:",
|
|
63
|
+
"",
|
|
64
|
+
...datasources.map((datasource) => {
|
|
65
|
+
const file = requireDefined(context.fileNames.get(datasource.id), `No doc file name for datasource ${datasource.id}`);
|
|
66
|
+
return `- ${datasourceLabel(datasource)} (${datasource.id}), [${file}](./${file})`;
|
|
67
|
+
}),
|
|
68
|
+
""
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
function orderedFields(datasource) {
|
|
72
|
+
const primary = datasource.fields.filter((field) => field.id === datasource.primaryFieldId);
|
|
73
|
+
const rest = datasource.fields.filter((field) => field.id !== datasource.primaryFieldId);
|
|
74
|
+
return [...primary, ...rest];
|
|
75
|
+
}
|
|
76
|
+
function renderField(field, datasource, context) {
|
|
77
|
+
return [
|
|
78
|
+
displayName(field.name),
|
|
79
|
+
"",
|
|
80
|
+
...detail(field, datasource, context)
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
/** The options that change what a value means or what a valid one looks like, and no others. */
|
|
84
|
+
const DETAILED_OPTIONS = [
|
|
85
|
+
{
|
|
86
|
+
key: "isoCode",
|
|
87
|
+
label: "Currency"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: "precision",
|
|
91
|
+
label: "Precision"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: "min",
|
|
95
|
+
label: "Min"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "max",
|
|
99
|
+
label: "Max"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: "format",
|
|
103
|
+
label: "Format"
|
|
104
|
+
}
|
|
105
|
+
];
|
|
106
|
+
function detail(field, datasource, context) {
|
|
107
|
+
const lines = [`- ID: ${field.id}`, `- Type: ${fieldTypeLiteral(field).slice(1, -1)}`];
|
|
108
|
+
if (field.id === datasource.primaryFieldId) lines.push("- Primary field");
|
|
109
|
+
const chain = underlyingType(field);
|
|
110
|
+
if (chain.includes("->")) lines.push(`- Resolves: ${chain}`);
|
|
111
|
+
lines.push(...optionLines(field));
|
|
112
|
+
lines.push(...choiceLines(field));
|
|
113
|
+
lines.push(...linkLine(field, context));
|
|
114
|
+
const applied = flags(field);
|
|
115
|
+
if (applied.length > 0) lines.push(`- Flags: ${applied.join(", ")}`);
|
|
116
|
+
if (field.defaultValue !== null && field.defaultValue !== void 0) lines.push(`- Default: ${code(JSON.stringify(field.defaultValue))}`);
|
|
117
|
+
lines.push(...formulaLines(field));
|
|
118
|
+
lines.push(`- Example: ${code(readValueExample(field, context.datasources))}`);
|
|
119
|
+
return lines;
|
|
120
|
+
}
|
|
121
|
+
function flags(field) {
|
|
122
|
+
const applied = [];
|
|
123
|
+
if (field.required) applied.push("required");
|
|
124
|
+
if (!isWritable(field)) applied.push("readonly");
|
|
125
|
+
if (field.createOnly) applied.push("createOnly");
|
|
126
|
+
if (field.aiOnly === true) applied.push("aiOnly");
|
|
127
|
+
return applied;
|
|
128
|
+
}
|
|
129
|
+
function optionLines(field) {
|
|
130
|
+
const { options } = resolvedShape(field);
|
|
131
|
+
return DETAILED_OPTIONS.flatMap(({ key, label }) => {
|
|
132
|
+
const value = option(options, key);
|
|
133
|
+
if (value === null || value === void 0) return [];
|
|
134
|
+
assert(typeof value !== "object", `Option ${key} on field ${field.id} is not a scalar`);
|
|
135
|
+
return [`- ${label}: ${String(value)}`];
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function linkLine(field, context) {
|
|
139
|
+
const linkedId = option(field.options, "linkedTableId");
|
|
140
|
+
if (typeof linkedId !== "string") return [];
|
|
141
|
+
const linked = context.datasources.get(linkedId);
|
|
142
|
+
if (linked === void 0) return [`- Links to: ${linkedId}, not registered`];
|
|
143
|
+
return [`- Links to: ${docLink(linked, context)} (${linked.id})`];
|
|
144
|
+
}
|
|
145
|
+
function choiceLines(field) {
|
|
146
|
+
const choices = choiceEntries(resolvedShape(field).options);
|
|
147
|
+
if (choices === null) return [];
|
|
148
|
+
return ["- Choices:", ...choices.map(({ id, label }) => ` - ${id} ${displayName(label)}`)];
|
|
149
|
+
}
|
|
150
|
+
function formulaLines(field) {
|
|
151
|
+
const formula = option(field.options, "formula");
|
|
152
|
+
if (typeof formula !== "string" || formula.includes("```")) return [];
|
|
153
|
+
return [
|
|
154
|
+
"- Formula:",
|
|
155
|
+
" ```",
|
|
156
|
+
...formula.split("\n").map((line) => ` ${line}`),
|
|
157
|
+
" ```"
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
function exampleFieldKeys(datasource) {
|
|
161
|
+
const keys = /* @__PURE__ */ new Map();
|
|
162
|
+
const used = /* @__PURE__ */ new Set();
|
|
163
|
+
for (const field of datasource.fields) {
|
|
164
|
+
const base = camelCase(field.name) || camelCase(field.id) || "field";
|
|
165
|
+
let key = base;
|
|
166
|
+
let suffix = 1;
|
|
167
|
+
while (used.has(key)) {
|
|
168
|
+
suffix += 1;
|
|
169
|
+
key = `${base}${suffix}`;
|
|
170
|
+
}
|
|
171
|
+
used.add(key);
|
|
172
|
+
keys.set(field.id, key);
|
|
173
|
+
}
|
|
174
|
+
return keys;
|
|
175
|
+
}
|
|
176
|
+
function renderExamples(datasource, datasources) {
|
|
177
|
+
const keys = exampleFieldKeys(datasource);
|
|
178
|
+
const keyOf = (field) => requireDefined(keys.get(field.id), `No example key for field ${field.id} on datasource ${datasource.id}`);
|
|
179
|
+
const queryId = camelCase(datasource.name) || camelCase(datasource.id);
|
|
180
|
+
assert(queryId !== "", `Datasource ${datasource.id} has no usable name`);
|
|
181
|
+
const name = pascalCase(datasource.name) || pascalCase(datasource.id);
|
|
182
|
+
const sampled = exampleFields(datasource);
|
|
183
|
+
const read = [
|
|
184
|
+
"## Read shape",
|
|
185
|
+
"",
|
|
186
|
+
"```ts",
|
|
187
|
+
"import { createServerFn } from '@tanstack/react-start'",
|
|
188
|
+
"import { defineQuery } from '@softr/sdk'",
|
|
189
|
+
"import { ds } from '@softr/sdk/server'",
|
|
190
|
+
"",
|
|
191
|
+
`const ${queryId} = defineQuery({`,
|
|
192
|
+
` id: ${quoted(queryId)},`,
|
|
193
|
+
` datasourceId: ${quoted(datasource.id)},`,
|
|
194
|
+
" fields: {",
|
|
195
|
+
...sampled.map((field) => ` ${keyOf(field)}: ${quoted(field.id)}, // ${fieldTypeLiteral(field).slice(1, -1)}`),
|
|
196
|
+
" },",
|
|
197
|
+
" permissions: { userGroup: 'ALL_USERS' },",
|
|
198
|
+
"})",
|
|
199
|
+
"",
|
|
200
|
+
`const list${name} = createServerFn().handler(async () => {`,
|
|
201
|
+
` return await ds.list(${queryId})`,
|
|
202
|
+
"})",
|
|
203
|
+
"```",
|
|
204
|
+
"",
|
|
205
|
+
"```ts",
|
|
206
|
+
"const fields = {",
|
|
207
|
+
...sampled.map((field) => ` ${keyOf(field)}: ${readValueExample(field, datasources)},`),
|
|
208
|
+
"}",
|
|
209
|
+
"```"
|
|
210
|
+
];
|
|
211
|
+
const writable = sampled.filter(isWritableExample);
|
|
212
|
+
if (writable.length === 0) return [...read, ""];
|
|
213
|
+
const mutationId = `create${name}`;
|
|
214
|
+
const write = [
|
|
215
|
+
"## Write shape",
|
|
216
|
+
"",
|
|
217
|
+
"```ts",
|
|
218
|
+
"import { createServerFn } from '@tanstack/react-start'",
|
|
219
|
+
"import { defineMutation } from '@softr/sdk'",
|
|
220
|
+
"import { ds } from '@softr/sdk/server'",
|
|
221
|
+
"",
|
|
222
|
+
`const ${mutationId} = defineMutation({`,
|
|
223
|
+
` id: ${quoted(mutationId)},`,
|
|
224
|
+
` datasourceId: ${quoted(datasource.id)},`,
|
|
225
|
+
" operation: 'create',",
|
|
226
|
+
" fields: {",
|
|
227
|
+
...writable.map((field) => ` ${keyOf(field)}: ${quoted(field.id)},`),
|
|
228
|
+
" },",
|
|
229
|
+
" permissions: { userGroup: 'LOGGED_IN_USERS' },",
|
|
230
|
+
"})",
|
|
231
|
+
"",
|
|
232
|
+
`const add${name} = createServerFn({ method: 'POST' }).handler(async () =>`,
|
|
233
|
+
` ds.create(${mutationId}, {`,
|
|
234
|
+
" data: {",
|
|
235
|
+
...writable.map((field) => ` ${keyOf(field)}: ${writeValueExample(field)},`),
|
|
236
|
+
" },",
|
|
237
|
+
" }),",
|
|
238
|
+
")",
|
|
239
|
+
"```",
|
|
240
|
+
""
|
|
241
|
+
];
|
|
242
|
+
return [
|
|
243
|
+
...read,
|
|
244
|
+
"",
|
|
245
|
+
...write
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The first field of every type the datasource has, because two fields of one type show one
|
|
250
|
+
* shape. `SELECT` and `SELECT[]` are two types, so both appear, and `orderedFields` puts the
|
|
251
|
+
* primary field first, so it is always the first of its own type.
|
|
252
|
+
*/
|
|
253
|
+
function exampleFields(datasource) {
|
|
254
|
+
const types = /* @__PURE__ */ new Set();
|
|
255
|
+
return orderedFields(datasource).filter((field) => {
|
|
256
|
+
const type = fieldTypeLiteral(field);
|
|
257
|
+
if (types.has(type)) return false;
|
|
258
|
+
types.add(type);
|
|
259
|
+
return true;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function isWritableExample(field) {
|
|
263
|
+
return isWritable(field) && field.aiOnly !== true;
|
|
264
|
+
}
|
|
265
|
+
function readValueExample(field, datasources) {
|
|
266
|
+
const choice = firstChoice(field);
|
|
267
|
+
if (choice !== null) return exampleLiteral(field, `{ id: ${quoted(choice.id)}, label: ${quoted(choice.label)} }`);
|
|
268
|
+
return exampleLiteral(field, readExample(field, datasources));
|
|
269
|
+
}
|
|
270
|
+
function writeValueExample(field) {
|
|
271
|
+
const choice = firstChoice(field);
|
|
272
|
+
if (choice !== null) return exampleLiteral(field, `{ id: ${quoted(choice.id)} }`);
|
|
273
|
+
return exampleLiteral(field, scalarExample(field));
|
|
274
|
+
}
|
|
275
|
+
function datasourceLabel(datasource) {
|
|
276
|
+
return displayName(datasource.name) || datasource.id;
|
|
277
|
+
}
|
|
278
|
+
/** A wider delimiter, because a value carrying a label can carry a backtick of its own. */
|
|
279
|
+
function code(value) {
|
|
280
|
+
return value.includes("`") ? `\`\` ${value} \`\`` : `\`${value}\``;
|
|
281
|
+
}
|
|
282
|
+
/** Names and labels arrive verbatim from the API, where a line break forges a row. */
|
|
283
|
+
function displayName(name) {
|
|
284
|
+
return name.replace(/\s+/g, " ").trim();
|
|
285
|
+
}
|
|
286
|
+
function quoted(value) {
|
|
287
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r")}'`;
|
|
288
|
+
}
|
|
289
|
+
function exampleLiteral(field, scalar) {
|
|
290
|
+
if (field.allowMultipleEntries) return scalar === null ? "[]" : `[${scalar}]`;
|
|
291
|
+
return scalar ?? "null";
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* A choice id the datasource declares, which is what the generated read type accepts. Read from
|
|
295
|
+
* the resolved shape, because the read type resolves a computed field the same way.
|
|
296
|
+
*/
|
|
297
|
+
function firstChoice(field) {
|
|
298
|
+
const shape = resolvedShape(field);
|
|
299
|
+
if (shape.type !== "SELECT") return null;
|
|
300
|
+
const choices = choiceEntries(shape.options);
|
|
301
|
+
return choices === null ? null : choices[0];
|
|
302
|
+
}
|
|
303
|
+
function camelCase(value) {
|
|
304
|
+
const parts = words(value);
|
|
305
|
+
if (parts.length === 0) return "";
|
|
306
|
+
const joined = parts.map((word, index) => index === 0 ? word[0].toLowerCase() + word.slice(1) : word[0].toUpperCase() + word.slice(1)).join("");
|
|
307
|
+
return /^[0-9]/.test(joined) ? `f${joined}` : joined;
|
|
308
|
+
}
|
|
309
|
+
function pascalCase(value) {
|
|
310
|
+
const joined = words(value).map((word) => word[0].toUpperCase() + word.slice(1)).join("");
|
|
311
|
+
return /^[0-9]/.test(joined) ? `F${joined}` : joined;
|
|
312
|
+
}
|
|
313
|
+
function words(value) {
|
|
314
|
+
return value.split(/[^A-Za-z0-9]+/).filter((word) => word.length > 0);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
export { renderDatasourceDoc, renderDatasourceIndex };
|
|
@@ -299,7 +299,7 @@ function fieldDoc(field, datasource, context, side) {
|
|
|
299
299
|
const lines = [`Name: ${field.name}`, `Underlying type: ${underlyingType(field)}`];
|
|
300
300
|
const tags = [];
|
|
301
301
|
if (field.defaultValue !== null && field.defaultValue !== void 0) tags.push(`@default ${JSON.stringify(field.defaultValue)}`);
|
|
302
|
-
const scalar = read ? readExample(field) : scalarExample(field);
|
|
302
|
+
const scalar = read ? readExample(field, context.datasources) : scalarExample(field);
|
|
303
303
|
if (scalar !== null) tags.push(`@example ${field.allowMultipleEntries ? `[${scalar}]` : scalar}`);
|
|
304
304
|
tags.push(...shapeTags(field));
|
|
305
305
|
if (field.id === datasource.primaryFieldId) tags.push("@primaryField");
|
|
@@ -373,21 +373,38 @@ function formulaTag(field) {
|
|
|
373
373
|
/** Stands in for any id in an example. The real ones are in the member type below it. */
|
|
374
374
|
const BLANK_ID = "00000000-0000-0000-0000-000000000000";
|
|
375
375
|
/** The read counterpart of {@link scalarExample}, wherever the API returns a different shape. */
|
|
376
|
-
function readExample(shape) {
|
|
376
|
+
function readExample(shape, datasources) {
|
|
377
377
|
switch (shape.type) {
|
|
378
378
|
case "SELECT": {
|
|
379
379
|
const choices = choiceEntries(shape.options);
|
|
380
380
|
return `{ id: '${BLANK_ID}', label: '${choices === null ? "text" : choices[0].label}' }`;
|
|
381
381
|
}
|
|
382
|
-
case "LINKED_RECORD": return `{ id: '${BLANK_ID}', label:
|
|
382
|
+
case "LINKED_RECORD": return `{ id: '${BLANK_ID}', label: ${linkedLabelExample(shape.options, datasources)} }`;
|
|
383
383
|
case "ATTACHMENT": return `{ id: '${BLANK_ID}', filename: 'file.pdf', type: 'application/pdf', url: 'https://example.com/file.pdf', expiresAt: '2026-03-14T09:30:00.000Z' }`;
|
|
384
384
|
case "LOCATION": return "'text'";
|
|
385
385
|
case "FORMULA":
|
|
386
386
|
case "LOOKUP":
|
|
387
|
-
case "ROLLUP": return readExample(returnedShape(shape));
|
|
387
|
+
case "ROLLUP": return readExample(returnedShape(shape), datasources);
|
|
388
388
|
default: return scalarExample(shape);
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* The label a read carries, resolved the way {@link linkedRecord} resolves its type: the linked
|
|
393
|
+
* table's primary field. `null` wherever that resolution fails, which says nothing about a table
|
|
394
|
+
* this app cannot see and inhabits every `LinkedRecord<T>` label.
|
|
395
|
+
*/
|
|
396
|
+
function linkedLabelExample(options, datasources) {
|
|
397
|
+
const linkedTableId = option(options, "linkedTableId");
|
|
398
|
+
if (typeof linkedTableId !== "string") return "null";
|
|
399
|
+
const linked = datasources.get(linkedTableId);
|
|
400
|
+
const primary = linked?.fields.find((field) => field.id === linked.primaryFieldId);
|
|
401
|
+
if (linked === void 0 || primary === void 0) return "null";
|
|
402
|
+
const rest = new Map(datasources);
|
|
403
|
+
rest.delete(linked.id);
|
|
404
|
+
const scalar = readExample(primary, rest);
|
|
405
|
+
if (scalar === null) return "null";
|
|
406
|
+
return primary.allowMultipleEntries ? `[${scalar}]` : scalar;
|
|
407
|
+
}
|
|
391
408
|
/** A literal showing the shape a write takes, or `null` where the shape is not settled. */
|
|
392
409
|
function scalarExample(shape) {
|
|
393
410
|
switch (shape.type) {
|
|
@@ -468,4 +485,4 @@ function indent(block) {
|
|
|
468
485
|
}
|
|
469
486
|
|
|
470
487
|
//#endregion
|
|
471
|
-
export { renderDatasourceTypes };
|
|
488
|
+
export { choiceEntries, fieldTypeLiteral, isWritable, option, readExample, renderDatasourceTypes, resolvedShape, scalarExample, underlyingType };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { panic } from "../assertions.js";
|
|
2
|
+
|
|
3
|
+
//#region src/codegen/doc-file-names.ts
|
|
4
|
+
function docFileNames(datasources) {
|
|
5
|
+
const bases = /* @__PURE__ */ new Map();
|
|
6
|
+
const seen = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const datasource of datasources) {
|
|
8
|
+
const base = slug(datasource.name) || slug(datasource.id);
|
|
9
|
+
if (base === "") panic(`Datasource ${datasource.id} has no usable name`);
|
|
10
|
+
bases.set(datasource.id, base);
|
|
11
|
+
const key = base.toLowerCase();
|
|
12
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
13
|
+
}
|
|
14
|
+
const names = /* @__PURE__ */ new Map();
|
|
15
|
+
/** The index the docs route the agent through, which a datasource must not overwrite. */
|
|
16
|
+
const taken = new Set(["readme"]);
|
|
17
|
+
for (const [id, base] of bases) {
|
|
18
|
+
const candidate = seen.get(base.toLowerCase()) === 1 ? base : `${base}-${id}`;
|
|
19
|
+
let name = candidate;
|
|
20
|
+
for (let attempt = 2; taken.has(name.toLowerCase()); attempt++) name = `${candidate}-${attempt}`;
|
|
21
|
+
taken.add(name.toLowerCase());
|
|
22
|
+
names.set(id, `${name}.md`);
|
|
23
|
+
}
|
|
24
|
+
return names;
|
|
25
|
+
}
|
|
26
|
+
function slug(value) {
|
|
27
|
+
return value.split(/[^A-Za-z0-9]+/).filter((word) => word.length > 0).join("-");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { docFileNames };
|
|
@@ -32,8 +32,11 @@ function header(sdkVersion) {
|
|
|
32
32
|
return `/**
|
|
33
33
|
* THIS FILE IS AUTO GENERATED, DO NOT EDIT THIS MANUALLY
|
|
34
34
|
*
|
|
35
|
+
* This file exists for type checking. See docs/datasources/ for field reference
|
|
36
|
+
* and guide how to use it.
|
|
37
|
+
*
|
|
35
38
|
* Generated by @softr/sdk@${sdkVersion}
|
|
36
|
-
* This file is regenerated automatically
|
|
39
|
+
* This file is regenerated automatically when a change is made to the datasource
|
|
37
40
|
*/
|
|
38
41
|
|
|
39
42
|
export {};`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requireDefined } from "../assertions.js";
|
|
2
|
+
import { renderDatasourceDoc, renderDatasourceIndex } from "./datasource-docs.js";
|
|
3
|
+
import { docFileNames } from "./doc-file-names.js";
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/codegen/write-datasource-docs.ts
|
|
8
|
+
/**
|
|
9
|
+
* Writes the index and one file per datasource, and touches nothing else in
|
|
10
|
+
* `docsDir`: the caller resolves it against `process.cwd()`, which can be any
|
|
11
|
+
* directory a developer ran the bin from.
|
|
12
|
+
*/
|
|
13
|
+
function writeDatasourceDocs(datasources, docsDir) {
|
|
14
|
+
const context = {
|
|
15
|
+
datasources: new Map(datasources.map((datasource) => [datasource.id, datasource])),
|
|
16
|
+
fileNames: docFileNames(datasources)
|
|
17
|
+
};
|
|
18
|
+
mkdirSync(docsDir, { recursive: true });
|
|
19
|
+
writeFileSync(path.join(docsDir, "README.md"), renderDatasourceIndex(datasources, context), "utf8");
|
|
20
|
+
for (const datasource of datasources) writeFileSync(path.join(docsDir, requireDefined(context.fileNames.get(datasource.id))), renderDatasourceDoc(datasource, context), "utf8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { writeDatasourceDocs };
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@ description: >-
|
|
|
16
16
|
metadata:
|
|
17
17
|
type: sub-skill
|
|
18
18
|
library: softr-sdk
|
|
19
|
-
library_version: 0.0.
|
|
19
|
+
library_version: 0.0.42
|
|
20
20
|
---
|
|
21
21
|
|
|
22
22
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineVisibility`, the hooks and every type come from `@softr/sdk`. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.42
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.42
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `useCurrentUser`, `usePermissions` and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.42
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.42
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.42
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.0.
|
|
16
|
+
library_version: 0.0.42
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -183,8 +183,7 @@ A read the datasource turns down throws rather than coming back empty, and it th
|
|
|
183
183
|
|
|
184
184
|
## Typing
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
Queries you write are type safe and a type error is shown when there's a mistake. For an in depth reference on what fields every datasource has check own project's `docs/datasources/` directory.
|
|
188
187
|
|
|
189
188
|
## References
|
|
190
189
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Softr field types and what their values look like
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Your own tables are recapped in `docs/datasources/`, one generated file per registered datasource: every field with its name, id, type and constraints, choice ids included. This reference is the general mapping behind those files, for reading a record's values without opening them.
|
|
4
4
|
|
|
5
5
|
Three rules cover nullability:
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ ATTACHMENT: {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Computed fields read as whatever they return
|
|
42
|
-
FORMULA, LOOKUP, ROLLUP: the returned field's shape, resolved in
|
|
42
|
+
FORMULA, LOOKUP, ROLLUP: the returned field's shape, resolved in docs/datasources/
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Every scalar above is `| null` on the record unless the field allows multiple entries, in which case the value is an `Array<...>` of the scalar.
|
|
@@ -23,7 +23,7 @@ A `where` callback returns exactly one value. For more than one condition, wrap
|
|
|
23
23
|
|
|
24
24
|
## Operators by field type
|
|
25
25
|
|
|
26
|
-
The handle for each field carries the operators its declared type supports
|
|
26
|
+
The handle for each field carries the operators its declared type supports.
|
|
27
27
|
|
|
28
28
|
### Text fields
|
|
29
29
|
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.42
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `defineVisibility`, the error predicates and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.42
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|