@softr/sdk 0.0.25 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/action.d.ts +195 -0
  2. package/dist/action.js +21 -0
  3. package/dist/auth/isomorphic.d.ts +1 -2
  4. package/dist/auth/permissions.d.ts +7 -2
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +639 -0
  7. package/dist/codegen/datasource-types.js +471 -0
  8. package/dist/codegen/softr-env.js +103 -0
  9. package/dist/codegen/user-groups.js +11 -0
  10. package/dist/core/integration.js +1 -1
  11. package/dist/datasource/chart.d.ts +196 -0
  12. package/dist/datasource/chart.js +289 -0
  13. package/dist/datasource/datasource.d.ts +16 -0
  14. package/dist/datasource/datasource.js +17 -0
  15. package/dist/datasource/fields.d.ts +122 -0
  16. package/dist/datasource/fields.js +82 -0
  17. package/dist/datasource/filter.d.ts +30 -0
  18. package/dist/datasource/filter.js +146 -0
  19. package/dist/datasource/record.d.ts +138 -0
  20. package/dist/datasource/record.js +258 -0
  21. package/dist/index.d.ts +3 -5
  22. package/dist/index.js +2 -2
  23. package/dist/package.js +5 -0
  24. package/dist/routes/visibility.d.ts +1 -1
  25. package/dist/server.d.ts +1 -1
  26. package/dist/server.js +1 -1
  27. package/package.json +2 -1
  28. package/skills/softr-sdk/SKILL.md +6 -2
  29. package/skills/softr-sdk/auth/SKILL.md +1 -1
  30. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  31. package/skills/softr-sdk/data-mutations/SKILL.md +1 -1
  32. package/skills/softr-sdk/integrations/SKILL.md +140 -0
  33. package/skills/softr-sdk/reading-records/SKILL.md +1 -1
  34. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  35. package/dist/core/datasource.d.ts +0 -31
  36. package/dist/core/datasource.js +0 -226
  37. package/dist/core/query.d.ts +0 -354
  38. package/dist/core/query.js +0 -528
  39. package/dist/core/utils.js +0 -14
  40. package/dist/types.d.ts +0 -128
@@ -139,4 +139,4 @@ type VisibilityHandle<R extends RouteVisibility = RouteVisibility> = {
139
139
  */
140
140
  declare function defineVisibility<const R extends RouteVisibility>(rule: R): VisibilityHandle<R>;
141
141
  //#endregion
142
- export { AccessBehaviour, AccessCheck, AccessError, AccessReason, RouteVisibility, VisibilityHandle, defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError };
142
+ export { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError };
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { auth } from "./auth/server.js";
2
- import { ds } from "./core/datasource.js";
2
+ import { ds } from "./datasource/datasource.js";
3
3
  import { workflow } from "./core/workflow.js";
4
4
  import { integration } from "./core/integration.js";
5
5
  import "@tanstack/react-start/server-only";
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { auth } from "./auth/server.js";
2
- import { ds } from "./core/datasource.js";
2
+ import { ds } from "./datasource/datasource.js";
3
3
  import { workflow } from "./core/workflow.js";
4
4
  import { integration } from "./core/integration.js";
5
5
  import "@tanstack/react-start/server-only";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -8,6 +8,7 @@
8
8
  "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
9
9
  "./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
10
10
  },
11
+ "bin": { "softr-sdk": "./dist/cli.js" },
11
12
  "files": ["dist", "skills", "!skills/_artifacts"],
12
13
  "publishConfig": { "access": "public" },
13
14
  "scripts": {
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: core
13
13
  library: softr-sdk
14
- library_version: 0.0.25
14
+ library_version: 0.0.26
15
15
  ---
16
16
 
17
17
  # Softr SDK core
@@ -43,6 +43,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
43
43
  | Fetch records from a datasource | [reading-records/SKILL.md](./reading-records/SKILL.md) |
44
44
  | Create, update or delete a record | [data-mutations/SKILL.md](./data-mutations/SKILL.md) |
45
45
  | Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
46
+ | Call a connected third-party API (Slack, Stripe, Gmail, …) | [integrations/SKILL.md](./integrations/SKILL.md) |
46
47
 
47
48
  ## Quick Decision Tree
48
49
 
@@ -62,6 +63,9 @@ Submitting a form, editing a record, or deleting one?
62
63
  Wiring a login, signup, or sign-out?
63
64
  load softr-sdk/auth
64
65
 
66
+ Calling a connected third-party API (Slack, Stripe, Gmail, Google Calendar, …)?
67
+ load softr-sdk/integrations
68
+
65
69
  Writing a server function, loader, or middleware in general?
66
70
  not a Softr concern. Use the TanStack Start skills.
67
71
  ```
@@ -124,4 +128,4 @@ type User = {
124
128
 
125
129
  ## Version
126
130
 
127
- Targets `@softr/sdk` v0.0.25.
131
+ Targets `@softr/sdk` v0.0.26.
@@ -16,7 +16,7 @@ description: >-
16
16
  metadata:
17
17
  type: sub-skill
18
18
  library: softr-sdk
19
- library_version: 0.0.25
19
+ library_version: 0.0.26
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.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.25
13
+ library_version: 0.0.26
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.
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.25
11
+ library_version: 0.0.26
12
12
  ---
13
13
 
14
14
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `q` 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.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: integrations
3
+ description: >-
4
+ Call a third-party service connected to the app as an integration: integration.fetch
5
+ from @softr/sdk/server, reading the datasource id and integration type from the
6
+ "Attached data sources" prompt section, looking up the base URL from the reference
7
+ table by integrationType, and constructing the full API URL from base URL plus the
8
+ path and query params the model knows from training. Check res.ok before res.json().
9
+ metadata:
10
+ type: sub-skill
11
+ library: softr-sdk
12
+ library_version: 0.0.26
13
+ ---
14
+
15
+ **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
16
+
17
+ # Integrations
18
+
19
+ `integration.fetch` calls a third-party API through Softr's authenticated proxy. It handles authorization automatically — you never set API keys or auth headers yourself.
20
+
21
+ ## Setup
22
+
23
+ ```ts
24
+ import { createServerFn } from '@tanstack/react-start'
25
+ import { integration } from '@softr/sdk/server'
26
+ ```
27
+
28
+ ## Reading connected integrations from the prompt
29
+
30
+ When the user attaches integrations to the chat, they appear in the `## Attached data sources` section of the prompt in this form:
31
+
32
+ ```
33
+ - "Gmail" — database `<databaseId>`, integration `<integrationId>`, type `GMAIL`
34
+ ```
35
+
36
+ Use `integrationId` as the first argument to `integration.fetch`. Use `integrationType` (e.g. `GMAIL`) to look up the base URL from the reference table below — then construct the full URL by appending the path and query parameters your training knows for that API.
37
+
38
+ ## Core pattern
39
+
40
+ ```ts
41
+ const getCalendarEvents = createServerFn().handler(async () => {
42
+ const now = new Date().toISOString()
43
+ const res = await integration.fetch(
44
+ '25d60213-ef4f-4fa0-b094-a46931b2d120',
45
+ `https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${encodeURIComponent(now)}&singleEvents=true&orderBy=startTime&maxResults=10`,
46
+ )
47
+ if (!res.ok) throw new Error(`Integration request failed: ${res.status} ${await res.text()}`)
48
+ return res.json()
49
+ })
50
+ ```
51
+
52
+ The integration id (`25d60213-…`) is the `integrationId` from the attached data sources section. The base URL (`https://www.googleapis.com`) is looked up from the reference table below by matching `integrationType` (`GOOGLE_CALENDAR`). The path and query params come from your knowledge of the Google Calendar REST API.
53
+
54
+ ## POST request
55
+
56
+ ```ts
57
+ const sendSlackMessage = createServerFn({ method: 'POST' })
58
+ .validator((data: { channel: string; text: string }) => data)
59
+ .handler(async ({ data }) => {
60
+ const res = await integration.fetch(
61
+ 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
62
+ 'https://slack.com/api/chat.postMessage',
63
+ {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({ channel: data.channel, text: data.text }),
67
+ },
68
+ )
69
+ if (!res.ok) throw new Error(`Slack error: ${res.status} ${await res.text()}`)
70
+ return res.json()
71
+ })
72
+ ```
73
+
74
+ `integration.fetch` accepts the same `RequestInit` options as the native `fetch`. Do not pass `Authorization` headers — Softr injects the integration's credentials automatically.
75
+
76
+ ## Base URLs by integration type
77
+
78
+ Match the `integrationType` from the attached data sources section to find the base URL:
79
+
80
+ | integrationType | Base URL |
81
+ |---|---|
82
+ | ANTHROPIC_AI | `https://api.anthropic.com` |
83
+ | APOLLO | `https://api.apollo.io` |
84
+ | APIFY | `https://api.apify.com` |
85
+ | ASANA | `https://app.asana.com` |
86
+ | ATTIO | `https://api.attio.com` |
87
+ | BASEROW | `https://api.baserow.io` |
88
+ | BITLY | `https://api-ssl.bitly.com` |
89
+ | BREVO | `https://api.brevo.com` |
90
+ | CALCOM | `https://api.cal.com` |
91
+ | CALENDLY | `https://api.calendly.com` |
92
+ | COHERE | `https://api.cohere.com` |
93
+ | DEEPL | `https://api.deepl.com` |
94
+ | DEEP_SEEK_AI | `https://api.deepseek.com` |
95
+ | DOCS_AUTOMATOR | `https://api.docsautomator.co` |
96
+ | DOCUMINT | `https://api.documint.me` |
97
+ | ELASTIC_EMAIL | `https://api.elasticemail.com` |
98
+ | ELEVEN_LABS | `https://api.elevenlabs.io` |
99
+ | FATHOM | `https://api.fathom.ai` |
100
+ | FIRECRAWL | `https://api.firecrawl.dev` |
101
+ | GEMINI_AI | `https://generativelanguage.googleapis.com` |
102
+ | GMAIL | `https://gmail.googleapis.com` |
103
+ | GOOGLE_CALENDAR | `https://www.googleapis.com` |
104
+ | GRANOLA | `https://public-api.granola.ai` |
105
+ | HUNTER | `https://api.hunter.io` |
106
+ | INSTANTLY | `https://api.instantly.ai` |
107
+ | LEMLIST | `https://api.lemlist.com` |
108
+ | LINEAR | `https://api.linear.app` |
109
+ | LINKUP | `https://api.linkup.so` |
110
+ | LOOPS | `https://app.loops.so` |
111
+ | MESSAGE_BIRD | `https://rest.messagebird.com` |
112
+ | MISTRAL_AI | `https://api.mistral.ai` |
113
+ | OPEN_AI | `https://api.openai.com` |
114
+ | OUTLOOK | `https://graph.microsoft.com` |
115
+ | PADDLE | `https://api.paddle.com` |
116
+ | PINECONE | `https://api.pinecone.io` |
117
+ | PIPELINE_CRM | `https://api.pipelinecrm.com` |
118
+ | POSTMARK | `https://api.postmarkapp.com` |
119
+ | REPLICATE | `https://api.replicate.com` |
120
+ | RESEND | `https://api.resend.com` |
121
+ | SLACK | `https://slack.com/api` |
122
+ | SMARTLEAD | `https://server.smartlead.ai` |
123
+ | STRIPE | `https://api.stripe.com` |
124
+ | TELEGRAM | `https://api.telegram.org` |
125
+ | TINYURL | `https://api.tinyurl.com` |
126
+ | TWILIO | `https://api.twilio.com` |
127
+ | XERO | `https://api.xero.com` |
128
+ | ZOOM | `https://api.zoom.us` |
129
+
130
+ ## When the service is not a Softr integration
131
+
132
+ If the API the user needs is not in the table above, call it directly from a server function using the native `fetch`. The user supplies their own credentials and the call lives entirely in the server function handler.
133
+
134
+ ## Failure modes
135
+
136
+ **Calling from client code.** `integration` is server-only. Importing it into a component or calling it outside a `createServerFn` handler fails the build or has no request context to act on.
137
+
138
+ **Setting Authorization headers.** Softr injects credentials automatically. A manually set `Authorization` header overrides the injected one and the request is rejected by the third-party API.
139
+
140
+ **Not checking `res.ok`.** `integration.fetch` returns the proxied response as-is. A 4xx or 5xx from the third-party API is not thrown — call `res.ok` or `res.status` before calling `res.json()`.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.25
13
+ library_version: 0.0.26
14
14
  ---
15
15
 
16
16
  **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. `q` 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.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.25
15
+ library_version: 0.0.26
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.
@@ -1,31 +0,0 @@
1
- import { ChartOptions, ChartResult, CreateOptions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrRecord, UpdateOptions } from "../types.js";
2
-
3
- //#region src/core/datasource.d.ts
4
- declare function listRecords<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options?: Omit<ListOptions, 'select'> & {
5
- select?: Sel;
6
- }): Promise<ListResult<SoftrRecord<RecordFieldsFor<Id, Sel>>>>;
7
- declare function getRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<GetOptions, 'select'> & {
8
- select?: Sel;
9
- }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>> | null>;
10
- declare function createRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<CreateOptions, 'select' | 'data'> & {
11
- select?: Sel;
12
- data: Partial<RecordFieldsFor<Id, Sel>>;
13
- }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>>>;
14
- declare function updateRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<UpdateOptions, 'select' | 'data'> & {
15
- select?: Sel;
16
- data: Partial<RecordFieldsFor<Id, Sel>>;
17
- }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>>>;
18
- declare function deleteRecord(datasourceId: string, id: string): Promise<void>;
19
- declare function getMetric(datasourceId: string, options: MetricOptions): Promise<MetricResult>;
20
- declare function getChart(datasourceId: string, options: ChartOptions): Promise<ChartResult>;
21
- declare const ds: {
22
- list: typeof listRecords;
23
- get: typeof getRecord;
24
- create: typeof createRecord;
25
- update: typeof updateRecord;
26
- delete: typeof deleteRecord;
27
- metric: typeof getMetric;
28
- chart: typeof getChart;
29
- };
30
- //#endregion
31
- export { ds };
@@ -1,226 +0,0 @@
1
- import { buildFilterCriteria, buildOrderBy, dealiasFilterCriteria, dealiasOrderBy } from "./query.js";
2
- import { panic } from "../assertions.js";
3
- import { readUserTimezone } from "../cookies.js";
4
- import { sessionConfig } from "../auth/config.js";
5
- import { readSessionToken } from "../auth/session.js";
6
- import { assertResponseOk } from "./utils.js";
7
-
8
- //#region src/core/datasource.ts
9
- const MAX_RECORD_COUNT = 100;
10
- const COUNT_CNT = "COUNT_CNT";
11
- const dsUrl = (datasourceId) => {
12
- const datasourceApiUrl = process.env.SOFTR_DATASOURCE_API_URL;
13
- if (!datasourceApiUrl) panic("SOFTR_DATASOURCE_API_URL is not set.");
14
- const { applicationId } = sessionConfig();
15
- return `${withoutTrailingSlash(datasourceApiUrl)}/v1/datasource/vibe/applications/${applicationId}/datasources/${datasourceId}`;
16
- };
17
- async function listRecords(datasourceId, options = {}) {
18
- const { select, where, orderBy } = options;
19
- const count = Math.min(options.limit ?? 20, MAX_RECORD_COUNT);
20
- const offset = options.offset ?? null;
21
- const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
22
- const sortingOption = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {}) : void 0;
23
- const res = await fetch(`${dsUrl(datasourceId)}/records`, {
24
- method: "POST",
25
- headers: {
26
- "Content-Type": "application/json",
27
- "X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
28
- "Authorization": `Bearer ${readSessionToken()}`
29
- },
30
- body: JSON.stringify({
31
- options: { userLocale: "en-US" },
32
- filterCriteria,
33
- sortingOption,
34
- pagingOption: {
35
- offset,
36
- count
37
- }
38
- })
39
- });
40
- await assertResponseOk(res, `Softr datasource list failed for "${datasourceId}"`);
41
- const payload = await res.json();
42
- const nextOffset = payload.offset < payload.total ? payload.offset : null;
43
- return {
44
- records: payload.items.map((item) => ({
45
- id: item.id,
46
- fields: select ? aliasFields(item.fields, select) : item.fields
47
- })),
48
- total: payload.total,
49
- nextOffset
50
- };
51
- }
52
- async function getRecord(datasourceId, options) {
53
- if (options.id !== void 0) {
54
- if (!options.id) throw new Error(`Softr datasource get: id must not be empty`);
55
- const res = await fetch(`${dsUrl(datasourceId)}/records/${options.id}`, {
56
- method: "POST",
57
- headers: {
58
- "Content-Type": "application/json",
59
- "X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
60
- "Authorization": `Bearer ${readSessionToken()}`
61
- },
62
- body: JSON.stringify({ options: { userLocale: "en-US" } })
63
- });
64
- await assertResponseOk(res, `Softr datasource get failed for "${datasourceId}"`);
65
- const contentLength = res.headers.get("Content-Length");
66
- if (contentLength && parseInt(contentLength, 10) === 0) return null;
67
- const record = await res.json();
68
- return {
69
- id: record.id,
70
- fields: options.select ? aliasFields(record.fields, options.select) : record.fields
71
- };
72
- }
73
- return (await listRecords(datasourceId, {
74
- where: options.where,
75
- select: options.select,
76
- limit: 1,
77
- timeZone: options.timeZone
78
- })).records[0] ?? null;
79
- }
80
- async function createRecord(datasourceId, options) {
81
- const rawFields = options.select ? dealiasData(options.data, options.select) : options.data;
82
- const res = await fetch(`${dsUrl(datasourceId)}/records/new`, {
83
- method: "POST",
84
- headers: {
85
- "Content-Type": "application/json",
86
- "Authorization": `Bearer ${readSessionToken()}`
87
- },
88
- body: JSON.stringify({ fields: rawFields })
89
- });
90
- await assertResponseOk(res, `Softr datasource create failed for "${datasourceId}"`);
91
- const record = await res.json();
92
- return {
93
- id: record.id,
94
- fields: options.select ? aliasFields(record.fields, options.select) : record.fields
95
- };
96
- }
97
- async function updateRecord(datasourceId, options) {
98
- if (!options.id) throw new Error(`Softr datasource update: id must not be empty`);
99
- const rawFields = options.select ? dealiasData(options.data, options.select) : options.data;
100
- const res = await fetch(`${dsUrl(datasourceId)}/records/${options.id}`, {
101
- method: "PATCH",
102
- headers: {
103
- "Content-Type": "application/json",
104
- "Authorization": `Bearer ${readSessionToken()}`
105
- },
106
- body: JSON.stringify({ fields: rawFields })
107
- });
108
- await assertResponseOk(res, `Softr datasource update failed for "${datasourceId}"`);
109
- const record = await res.json();
110
- return {
111
- id: record.id,
112
- fields: options.select ? aliasFields(record.fields, options.select) : record.fields
113
- };
114
- }
115
- async function deleteRecord(datasourceId, id) {
116
- await assertResponseOk(await fetch(`${dsUrl(datasourceId)}/records/${id}`, {
117
- method: "DELETE",
118
- headers: { "Authorization": `Bearer ${readSessionToken()}` }
119
- }), `Softr datasource delete failed for "${datasourceId}"`);
120
- }
121
- async function getMetric(datasourceId, options) {
122
- const { select, where, aggregation, field } = options;
123
- if (aggregation !== "COUNT" && !field) throw new Error(`field is required for aggregation "${aggregation}"`);
124
- const fieldId = field ? select?.[field] ?? field : void 0;
125
- const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
126
- const res = await fetch(`${dsUrl(datasourceId)}/chart/data`, {
127
- method: "POST",
128
- headers: {
129
- "Content-Type": "application/json",
130
- "X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
131
- "Authorization": `Bearer ${readSessionToken()}`
132
- },
133
- body: JSON.stringify({
134
- options: { userLocale: "en-US" },
135
- groupBy: [],
136
- metrics: [{
137
- field: fieldId,
138
- aggregation
139
- }],
140
- filterCriteria
141
- })
142
- });
143
- await assertResponseOk(res, `Softr datasource metric failed for "${datasourceId}"`);
144
- const payload = await res.json();
145
- const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
146
- let value;
147
- const responseKey = aggregation === "COUNT" && !fieldId ? COUNT_CNT : fieldId;
148
- value = payload.data.find((item) => responseKey in item)?.[responseKey] ?? null;
149
- return {
150
- value,
151
- executionTimestamp
152
- };
153
- }
154
- async function getChart(datasourceId, options) {
155
- const { select, where, groupBy, metrics, orderBy } = options;
156
- const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
157
- const sortingOptions = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {}) : void 0;
158
- const groupByFields = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((field) => ({ field: select?.[field] ?? field }));
159
- const yAxisFields = Object.values(metrics).map(({ field, aggregation }) => ({
160
- field: field ? select?.[field] ?? field : void 0,
161
- aggregation
162
- }));
163
- if (!yAxisFields.length) throw new Error("chart requires at least one metric");
164
- const res = await fetch(`${dsUrl(datasourceId)}/chart/data`, {
165
- method: "POST",
166
- headers: {
167
- "Content-Type": "application/json",
168
- "X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
169
- "Authorization": `Bearer ${readSessionToken()}`
170
- },
171
- body: JSON.stringify({
172
- options: { userLocale: "en-US" },
173
- filterCriteria,
174
- sortingOptions,
175
- groupBy: groupByFields,
176
- metrics: yAxisFields
177
- })
178
- });
179
- await assertResponseOk(res, `Softr datasource chart failed for "${datasourceId}"`);
180
- const payload = await res.json();
181
- const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
182
- const aliases = {};
183
- const multipleMetrics = Object.keys(metrics).length > 1;
184
- for (const [alias, { field, aggregation }] of Object.entries(metrics)) if (aggregation === "COUNT") aliases[COUNT_CNT] = alias;
185
- else {
186
- const fieldId = field ? select?.[field] ?? field : field;
187
- if (fieldId) {
188
- const suffix = multipleMetrics ? `_${aggregation.toLowerCase()}` : "";
189
- aliases[fieldId + suffix] = alias;
190
- }
191
- }
192
- for (const field of Array.isArray(groupBy) ? groupBy : [groupBy]) aliases[select?.[field] ?? field] = field;
193
- return {
194
- data: payload.data.map((item) => {
195
- const row = {};
196
- for (const [rawKey, alias] of Object.entries(aliases)) if (rawKey in item) row[alias] = item[rawKey];
197
- return row;
198
- }),
199
- executionTimestamp
200
- };
201
- }
202
- function aliasFields(fields, aliases) {
203
- const result = {};
204
- for (const [alias, fieldId] of Object.entries(aliases)) result[alias] = fields[fieldId];
205
- return result;
206
- }
207
- function dealiasData(data, select) {
208
- const result = {};
209
- for (const [alias, value] of Object.entries(data)) result[select[alias] ?? alias] = value;
210
- return result;
211
- }
212
- const ds = {
213
- list: listRecords,
214
- get: getRecord,
215
- create: createRecord,
216
- update: updateRecord,
217
- delete: deleteRecord,
218
- metric: getMetric,
219
- chart: getChart
220
- };
221
- function withoutTrailingSlash(url) {
222
- return url.replace(/\/+$/, "");
223
- }
224
-
225
- //#endregion
226
- export { ds, dsUrl };