@softr/sdk 0.0.41 → 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 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: 'text' }`;
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 at the end of the turn
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
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.41";
2
+ var version = "0.0.42";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: core
13
13
  library: softr-sdk
14
- library_version: 0.0.41
14
+ library_version: 0.0.42
15
15
  ---
16
16
 
17
17
  # Softr SDK core
@@ -16,7 +16,7 @@ description: >-
16
16
  metadata:
17
17
  type: sub-skill
18
18
  library: softr-sdk
19
- library_version: 0.0.41
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.41
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.41
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.41
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.41
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.41
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.41
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
- `softr-env.d.ts`, the generated file at the app root, types records, filters and sorts from every registered datasource. It follows the data on its own: nothing to run, nothing to edit. It checks the ids you wrote rather than handing them to you, so ask the schema for a field id and open the file only when a declaration will not compile.
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
- The generated `softr-env.d.ts` is the authority for your own tables: it types every field of every registered datasource, choice ids included, with the field's name and constraints in a docstring. This reference is the general mapping behind it, for reading a record's values without opening that file.
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 softr-env.d.ts
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, taken from the generated `softr-env.d.ts`.
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.41
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.41
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.