@softr/sdk 0.0.57 → 0.0.58-alpha.1

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.
@@ -1,5 +1,5 @@
1
1
  import { assert, requireDefined } from "../assertions.js";
2
- import { choiceEntries, fieldTypeLiteral, isWritable, option, readExample, resolvedShape, scalarExample, underlyingType } from "./datasource-types.js";
2
+ import { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, resolvedShape, scalarExample, underlyingType } from "./datasource-types.js";
3
3
 
4
4
  //#region src/codegen/datasource-docs.ts
5
5
  const DOC_NOTE = "> This document is generated automatically when a change is made in the datasource.";
@@ -35,7 +35,7 @@ function relatedLinks(datasource, context) {
35
35
  const linkedId = option(field.options, "linkedTableId");
36
36
  if (typeof linkedId !== "string" || seen.has(linkedId)) continue;
37
37
  seen.add(linkedId);
38
- const linked = context.datasources.get(linkedId);
38
+ const linked = linkedDatasource(linkedId, context.datasources);
39
39
  if (linked === void 0 || linked.id === datasource.id) continue;
40
40
  links.push(docLink(linked, context));
41
41
  }
@@ -138,7 +138,7 @@ function optionLines(field) {
138
138
  function linkLine(field, context) {
139
139
  const linkedId = option(field.options, "linkedTableId");
140
140
  if (typeof linkedId !== "string") return [];
141
- const linked = context.datasources.get(linkedId);
141
+ const linked = linkedDatasource(linkedId, context.datasources);
142
142
  if (linked === void 0) return [`- Links to: ${linkedId}, not registered`];
143
143
  return [`- Links to: ${docLink(linked, context)} (${linked.id})`];
144
144
  }
@@ -0,0 +1,65 @@
1
+ import { linkedDatasource, resolvedShape } from "./datasource-types.js";
2
+
3
+ //#region src/codegen/datasource-schema.ts
4
+ /**
5
+ * The types a `contains` filter accepts, so a linked picker knows whether it can search its label.
6
+ * Kept in step with `TextFieldType` in ../datasource/fields.ts, which is not exported.
7
+ */
8
+ const SEARCHABLE_TYPES = new Set([
9
+ "SINGLE_LINE_TEXT",
10
+ "LONG_TEXT",
11
+ "URL",
12
+ "EMAIL",
13
+ "PHONE",
14
+ "ADDRESS",
15
+ "RECORD_ID",
16
+ "LOCATION",
17
+ "TIME",
18
+ "BUTTON"
19
+ ]);
20
+ /**
21
+ * One datasource's metadata as JSON, for tools that generate code from it.
22
+ *
23
+ * A linked field carries its target already resolved: the registration id and the field a person
24
+ * recognises a row by. Readers never map table ids themselves. `datasources` is keyed by
25
+ * registration id.
26
+ */
27
+ function renderDatasourceSchema(datasource, datasources) {
28
+ const schema = {
29
+ id: datasource.id,
30
+ name: datasource.name,
31
+ primaryFieldId: datasource.primaryFieldId,
32
+ fields: datasource.fields.map((field) => schemaField(field, datasources))
33
+ };
34
+ return `${JSON.stringify(schema, null, 2)}\n`;
35
+ }
36
+ function schemaField(field, datasources) {
37
+ const base = {
38
+ id: field.id,
39
+ name: field.name,
40
+ type: field.type,
41
+ allowMultipleEntries: field.allowMultipleEntries,
42
+ required: field.required,
43
+ readonly: field.readonly,
44
+ createOnly: field.createOnly,
45
+ aiOnly: field.aiOnly === true,
46
+ defaultValue: field.defaultValue,
47
+ options: field.options
48
+ };
49
+ if (field.type !== "LINKED_RECORD") return base;
50
+ const linkedTableId = field.options?.["linkedTableId"];
51
+ const linked = typeof linkedTableId === "string" ? linkedDatasource(linkedTableId, datasources) : void 0;
52
+ if (linked === void 0 || linked.primaryFieldId === "") return base;
53
+ const primary = linked.fields.find((entry) => entry.id === linked.primaryFieldId);
54
+ return {
55
+ ...base,
56
+ linked: {
57
+ datasourceId: linked.id,
58
+ labelFieldId: linked.primaryFieldId,
59
+ labelSearchable: primary !== void 0 && SEARCHABLE_TYPES.has(resolvedShape(primary).type)
60
+ }
61
+ };
62
+ }
63
+
64
+ //#endregion
65
+ export { renderDatasourceSchema };
@@ -46,6 +46,15 @@ function byId(datasources) {
46
46
  return new Map(datasources.map((datasource) => [datasource.id, datasource]));
47
47
  }
48
48
  /**
49
+ * The registered datasource a `LINKED_RECORD`'s `linkedTableId` names. That id is the underlying
50
+ * table's own id, kept as `tableId`, while `datasources` is keyed by registration id. The
51
+ * registration id is the fallback, in case a link names that one instead.
52
+ */
53
+ function linkedDatasource(linkedTableId, datasources) {
54
+ for (const datasource of datasources.values()) if (datasource.tableId === linkedTableId) return datasource;
55
+ return datasources.get(linkedTableId);
56
+ }
57
+ /**
49
58
  * A TypeScript name per datasource, derived from its own name, with the id standing in wherever
50
59
  * the name cannot: two datasources can share a name, and a name can be blank or hold nothing an
51
60
  * identifier can keep. The id is unique by construction, so it settles both.
@@ -238,7 +247,7 @@ function union(members) {
238
247
  function linkedRecord(options, context) {
239
248
  const linkedTableId = option(options, "linkedTableId");
240
249
  if (typeof linkedTableId !== "string") return "LinkedRecord";
241
- const linked = context.datasources.get(linkedTableId);
250
+ const linked = linkedDatasource(linkedTableId, context.datasources);
242
251
  if (linked === void 0 || linked.primaryFieldId === "") return "LinkedRecord";
243
252
  return `LinkedRecord<NonNullable<${`${nameOf(linked.id, context)}Read[${quote(linked.primaryFieldId)}]`}>>`;
244
253
  }
@@ -339,7 +348,7 @@ function lookupTag(field, datasource, context) {
339
348
  const link = datasource.fields.find((candidate) => candidate.id === linkId);
340
349
  if (link === void 0) return null;
341
350
  const linkedTableId = option(link.options, "linkedTableId");
342
- const target = (typeof linkedTableId === "string" ? context.datasources.get(linkedTableId) : void 0)?.fields.find((candidate) => candidate.id === targetId);
351
+ const target = (typeof linkedTableId === "string" ? linkedDatasource(linkedTableId, context.datasources) : void 0)?.fields.find((candidate) => candidate.id === targetId);
343
352
  if (target === void 0) return null;
344
353
  return `@lookup ${link.name} -> ${target.name}`;
345
354
  }
@@ -396,7 +405,7 @@ function readExample(shape, datasources) {
396
405
  function linkedLabelExample(options, datasources) {
397
406
  const linkedTableId = option(options, "linkedTableId");
398
407
  if (typeof linkedTableId !== "string") return "null";
399
- const linked = datasources.get(linkedTableId);
408
+ const linked = linkedDatasource(linkedTableId, datasources);
400
409
  const primary = linked?.fields.find((field) => field.id === linked.primaryFieldId);
401
410
  if (linked === void 0 || primary === void 0) return "null";
402
411
  const rest = new Map(datasources);
@@ -485,4 +494,4 @@ function indent(block) {
485
494
  }
486
495
 
487
496
  //#endregion
488
- export { choiceEntries, fieldTypeLiteral, isWritable, option, readExample, renderDatasourceTypes, resolvedShape, scalarExample, underlyingType };
497
+ export { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, renderDatasourceTypes, resolvedShape, scalarExample, underlyingType };
@@ -22,9 +22,11 @@ async function fetchSoftrEnvInput(source) {
22
22
  const [registered, userGroups] = await Promise.all([apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/datasources`, token), apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/access-control/user-groups`, token)]);
23
23
  return {
24
24
  datasources: await Promise.all(registered.map(async ({ name, datasource }) => {
25
+ const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/vibe/studio/applications/${applicationId}/datasources/${datasource.id}/metadata`, token);
25
26
  return {
26
- ...await apiFetch(`${datasourceApiUrl}/v1/datasource/vibe/studio/applications/${applicationId}/datasources/${datasource.id}/metadata`, token),
27
+ ...metadata,
27
28
  id: datasource.id,
29
+ tableId: metadata.id,
28
30
  name
29
31
  };
30
32
  })),
@@ -1,12 +1,13 @@
1
1
  import { requireDefined } from "../assertions.js";
2
2
  import { renderDatasourceDoc, renderDatasourceIndex } from "./datasource-docs.js";
3
+ import { renderDatasourceSchema } from "./datasource-schema.js";
3
4
  import { docFileNames } from "./doc-file-names.js";
4
5
  import { writeIfChanged } from "./write-if-changed.js";
5
6
  import path from "node:path";
6
7
 
7
8
  //#region src/codegen/write-datasource-docs.ts
8
9
  /**
9
- * Writes the index and one file per datasource, and touches nothing else in
10
+ * Writes the index, and one doc and one schema JSON per datasource, and touches nothing else in
10
11
  * `docsDir`: the caller resolves it against `process.cwd()`, which can be any
11
12
  * directory a developer ran the bin from. `changed` is true when any file it
12
13
  * wrote now reads differently.
@@ -16,7 +17,11 @@ function writeDatasourceDocs(datasources, docsDir) {
16
17
  datasources: new Map(datasources.map((datasource) => [datasource.id, datasource])),
17
18
  fileNames: docFileNames(datasources)
18
19
  };
19
- const writes = [writeIfChanged(path.join(docsDir, "README.md"), renderDatasourceIndex(datasources, context)), ...datasources.map((datasource) => writeIfChanged(path.join(docsDir, requireDefined(context.fileNames.get(datasource.id))), renderDatasourceDoc(datasource, context)))];
20
+ const writes = [
21
+ writeIfChanged(path.join(docsDir, "README.md"), renderDatasourceIndex(datasources, context)),
22
+ ...datasources.map((datasource) => writeIfChanged(path.join(docsDir, requireDefined(context.fileNames.get(datasource.id))), renderDatasourceDoc(datasource, context))),
23
+ ...datasources.map((datasource) => writeIfChanged(path.join(docsDir, requireDefined(context.fileNames.get(datasource.id)).replace(/\.md$/, ".json")), renderDatasourceSchema(datasource, context.datasources)))
24
+ ];
20
25
  return {
21
26
  fileNames: context.fileNames,
22
27
  changed: writes.includes(true)
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.57";
2
+ var version = "0.0.58-alpha.1";
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.57",
3
+ "version": "0.0.58-alpha.1",
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.57
14
+ library_version: 0.0.58-alpha.1
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.57
19
+ library_version: 0.0.58-alpha.1
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.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.57
15
+ library_version: 0.0.58-alpha.1
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.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.57
12
+ library_version: 0.0.58-alpha.1
13
13
  ---
14
14
 
15
15
  # The current user
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.57
13
+ library_version: 0.0.58-alpha.1
14
14
  ---
15
15
 
16
16
  **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.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.57
13
+ library_version: 0.0.58-alpha.1
14
14
  ---
15
15
 
16
16
  **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.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.57
12
+ library_version: 0.0.58-alpha.1
13
13
  ---
14
14
 
15
15
  **Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.57
12
+ library_version: 0.0.58-alpha.1
13
13
  ---
14
14
 
15
15
  # Installing the app
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: sub-skill
13
13
  library: softr-sdk
14
- library_version: 0.0.57
14
+ library_version: 0.0.58-alpha.1
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.
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.57
16
+ library_version: 0.0.58-alpha.1
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. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` and every type come 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.57
12
+ library_version: 0.0.58-alpha.1
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.57
16
+ library_version: 0.0.58-alpha.1
17
17
  ---
18
18
 
19
19
  **Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.57
12
+ library_version: 0.0.58-alpha.1
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.