@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
@@ -0,0 +1,82 @@
1
+ import { INTERNAL_KIND } from "./filter.js";
2
+
3
+ //#region src/datasource/fields.ts
4
+ function makeFieldSet(aliases) {
5
+ const set = {};
6
+ for (const alias of aliases) set[alias] = makeChain(alias);
7
+ set["$and"] = (...members) => group("AND", members);
8
+ set["$or"] = (...members) => group("OR", members);
9
+ return set;
10
+ }
11
+ function makeSortSet(aliases) {
12
+ const set = {};
13
+ for (const alias of aliases) set[alias] = {
14
+ asc: {
15
+ sortingField: alias,
16
+ sortType: "ASC",
17
+ [INTERNAL_KIND]: "ORDER_BY"
18
+ },
19
+ desc: {
20
+ sortingField: alias,
21
+ sortType: "DESC",
22
+ [INTERNAL_KIND]: "ORDER_BY"
23
+ }
24
+ };
25
+ return set;
26
+ }
27
+ /** Ands a list of filters into one group, which is what an accumulated `where` needs. */
28
+ function andAll(filters) {
29
+ return group("AND", filters);
30
+ }
31
+ function group(logicalOperator, filters) {
32
+ return {
33
+ logicalOperator,
34
+ filters,
35
+ [INTERNAL_KIND]: "LOGICAL_GROUP"
36
+ };
37
+ }
38
+ /**
39
+ * One chain per alias, carrying every operator whatever the alias reads as. Only the types
40
+ * narrow: a call the types reject cannot reach here, so there is nothing for a per-type
41
+ * runtime chain to catch.
42
+ */
43
+ function makeChain(subject) {
44
+ return {
45
+ is: (value) => leaf(subject, "IS", value),
46
+ isNot: (value) => leaf(subject, "IS_NOT", value),
47
+ isEmpty: () => leaf(subject, "IS_EMPTY"),
48
+ isNotEmpty: () => leaf(subject, "IS_NOT_EMPTY"),
49
+ contains: (value) => leaf(subject, "CONTAINS", value),
50
+ startsWith: (value) => leaf(subject, "STARTS_WITH", value),
51
+ endsWith: (value) => leaf(subject, "ENDS_WITH", value),
52
+ isOneOf: (values) => leaf(subject, "IS_ONE_OF", values),
53
+ isNoneOf: (values) => leaf(subject, "IS_NONE_OF", values),
54
+ hasAllOf: (values) => leaf(subject, "HAS_ALL_OF", values),
55
+ gt: (value) => leaf(subject, "GT", value),
56
+ gte: (value) => leaf(subject, "GTE", value),
57
+ lt: (value) => leaf(subject, "LT", value),
58
+ lte: (value) => leaf(subject, "LTE", value),
59
+ between: (start, end) => leaf(subject, "BETWEEN", [start, end]),
60
+ isNotBetween: (start, end) => leaf(subject, "IS_NOT_BETWEEN", [start, end])
61
+ };
62
+ }
63
+ function leaf(subject, operator, value) {
64
+ const probe = Array.isArray(value) ? value[0] : value;
65
+ if (typeof probe === "number") return {
66
+ subject,
67
+ operator,
68
+ value: Array.isArray(value) ? value.map(String) : String(value),
69
+ type: "NUMERIC",
70
+ [INTERNAL_KIND]: "FILTER"
71
+ };
72
+ return {
73
+ subject,
74
+ operator,
75
+ value,
76
+ type: typeof probe === "boolean" ? "BOOLEAN" : Array.isArray(value) ? "ARRAY" : "TEXT",
77
+ [INTERNAL_KIND]: "FILTER"
78
+ };
79
+ }
80
+
81
+ //#endregion
82
+ export { andAll, makeFieldSet, makeSortSet };
@@ -0,0 +1,30 @@
1
+ //#region src/datasource/filter.d.ts
2
+
3
+ type LogicalOperator = "AND" | "OR";
4
+ type FilterOperator = "IS" | "IS_NOT" | "EQUAL" | "NOT_EQUAL" | "IS_EMPTY" | "IS_NOT_EMPTY" | "IS_ONE_OF" | "IS_NONE_OF" | "HAS_ALL_OF" | "CONTAINS" | "INLINE_CONTAINS" | "GT" | "LT" | "GTE" | "LTE" | "BETWEEN" | "IS_NOT_BETWEEN" | "DOES_NOT_CONTAINS" | "STARTS_WITH" | "ENDS_WITH";
5
+ type ConditionFieldType = "TEXT" | "BOOLEAN" | "NUMERIC" | "ARRAY" | "DATE";
6
+ interface Filter {
7
+ subject: string;
8
+ operator: FilterOperator;
9
+ value?: unknown;
10
+ type: ConditionFieldType;
11
+ }
12
+ type ConstructableFilter = Filter & {
13
+ [INTERNAL_KIND]: "FILTER";
14
+ };
15
+ type ConstructableFilterGroup = {
16
+ logicalOperator: LogicalOperator;
17
+ filters: Array<ConstructableFilter | ConstructableFilterGroup>;
18
+ [INTERNAL_KIND]: "LOGICAL_GROUP";
19
+ };
20
+ interface SortingOptionItem {
21
+ sortingField: string;
22
+ sortType: "ASC" | "DESC";
23
+ }
24
+ type ConstructableOrderBy = SortingOptionItem & {
25
+ [INTERNAL_KIND]: "ORDER_BY";
26
+ };
27
+ type AnyConstructable = ConstructableFilter | ConstructableFilterGroup;
28
+ declare const INTERNAL_KIND: unique symbol;
29
+ //#endregion
30
+ export { AnyConstructable, ConstructableFilter, ConstructableFilterGroup, ConstructableOrderBy };
@@ -0,0 +1,146 @@
1
+ import { assert } from "../assertions.js";
2
+
3
+ //#region src/datasource/filter.ts
4
+ const INTERNAL_KIND = Symbol("KIND");
5
+ /**
6
+ * Converts filter utility conditions to a format our backend supports.
7
+ *
8
+ * A leaf becomes a group of one, because the wire format has no way to hold a bare
9
+ * condition and a group side by side at the same level.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const json = buildFilterCriteria(fields.title.contains("hello"));
14
+ * // result
15
+ * {
16
+ * "filterCriteria": {
17
+ * "filterConditionGroup": {
18
+ * "logicalOperator": "AND"
19
+ * "filterGroups": [{
20
+ * "logicalOperator": "AND",
21
+ * "filters": [{
22
+ * "subject": "title",
23
+ * "type": "TEXT",
24
+ * "operator": "CONTAINS",
25
+ * "value": "hello"
26
+ * }],
27
+ * }]
28
+ * }
29
+ * }
30
+ * }
31
+ *
32
+ * ```
33
+ */
34
+ function buildFilterCriteria(input) {
35
+ assert(INTERNAL_KIND in input, "a filter must come from the callback a read hands you");
36
+ if (isFilter(input)) return { filterConditionGroup: {
37
+ logicalOperator: "AND",
38
+ filterGroups: [{
39
+ logicalOperator: "AND",
40
+ filters: [stripInternalKind(input)]
41
+ }]
42
+ } };
43
+ if (isFilterGroup(input)) {
44
+ if (input.filters.length === 0) return { filterConditionGroup: {
45
+ logicalOperator: input.logicalOperator,
46
+ filterGroups: []
47
+ } };
48
+ return { filterConditionGroup: {
49
+ logicalOperator: input.logicalOperator,
50
+ filterGroups: input.filters.map((inner) => {
51
+ if (isFilter(inner)) return {
52
+ logicalOperator: "AND",
53
+ filters: [stripInternalKind(inner)]
54
+ };
55
+ if (!inner.filters.every(isFilter)) throw new Error("Cannot nest filter groups more than three levels deep.");
56
+ return {
57
+ logicalOperator: inner.logicalOperator,
58
+ filters: inner.filters.map(stripInternalKind)
59
+ };
60
+ })
61
+ } };
62
+ }
63
+ throw new Error("Unknown filter kind: " + input[INTERNAL_KIND]);
64
+ }
65
+ /**
66
+ * Converts order-by utility conditions to a format our backend supports
67
+ *
68
+ * @example
69
+ * single orderBy
70
+ * ```tsx
71
+ * const orderBy = buildOrderBy(sorts.title.asc);
72
+ * // result: [{ sortingField: "title", sortType: "ASC" }]
73
+ * ```
74
+ *
75
+ * @example
76
+ * multiple orderBys
77
+ * ```tsx
78
+ * const orderBy = buildOrderBy([sorts.createdAt.desc, sorts.title.asc]);
79
+ * // result: [
80
+ * // { sortingField: "createdAt", sortType: "DESC" },
81
+ * // { sortingField: "title", sortType: "ASC" }
82
+ * // ]
83
+ * ```
84
+ */
85
+ function buildOrderBy(input) {
86
+ const orderBys = Array.isArray(input) ? input : [input];
87
+ assert(orderBys.every((ob) => ob[INTERNAL_KIND] === "ORDER_BY"), "a sort must come from the callback a read hands you");
88
+ return orderBys.map((orderBy) => ({
89
+ sortingField: orderBy.sortingField,
90
+ sortType: orderBy.sortType
91
+ }));
92
+ }
93
+ function dealiasOrderBy(orderbys, aliases) {
94
+ return orderbys.map((orderBy) => {
95
+ const subject = aliases[orderBy.sortingField];
96
+ if (!subject) throw new Error(`Could not find an alias for subject "${orderBy.sortingField}"`);
97
+ return {
98
+ ...orderBy,
99
+ sortingField: subject
100
+ };
101
+ });
102
+ }
103
+ /**
104
+ * Replaces filter subjects with their aliased values.
105
+ *
106
+ * Throws an error if any filter subject does not have a corresponding alias in the provided mapping.
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * const criteria = buildFilterCriteria(fields.name.contains("John"));
111
+ * const aliases = { name: "abcxy123" };
112
+ * const dealiased = dealiasFilterCriteria(criteria, aliases);
113
+ * // Result: filter subject is now "abcxyz123" instead of "name"
114
+ * ```
115
+ */
116
+ function dealiasFilterCriteria(criteria, aliases) {
117
+ if (!criteria.filterConditionGroup) return criteria;
118
+ return { filterConditionGroup: {
119
+ ...criteria.filterConditionGroup,
120
+ filterGroups: criteria.filterConditionGroup.filterGroups.map((group) => ({
121
+ ...group,
122
+ filters: group.filters.map(dealiasFilter)
123
+ }))
124
+ } };
125
+ function dealiasFilter(filter) {
126
+ const subject = aliases[filter.subject];
127
+ assert(subject, () => `Could not find an alias for subject "${filter.subject}"`);
128
+ return {
129
+ ...filter,
130
+ subject
131
+ };
132
+ }
133
+ }
134
+ function stripInternalKind(item) {
135
+ const { [INTERNAL_KIND]: _,...rest } = item;
136
+ return rest;
137
+ }
138
+ function isFilterGroup(item) {
139
+ return item[INTERNAL_KIND] === "LOGICAL_GROUP";
140
+ }
141
+ function isFilter(item) {
142
+ return item[INTERNAL_KIND] === "FILTER";
143
+ }
144
+
145
+ //#endregion
146
+ export { INTERNAL_KIND, buildFilterCriteria, buildOrderBy, dealiasFilterCriteria, dealiasOrderBy };
@@ -0,0 +1,138 @@
1
+ import { Action, ActionData, ActionRecord, ActionShape, ReadShape } from "../action.js";
2
+ import { AnyConstructable, ConstructableOrderBy } from "./filter.js";
3
+ import { FieldSet, SortSet } from "./fields.js";
4
+
5
+ //#region src/datasource/record.d.ts
6
+ type GetOptions = {
7
+ id: string;
8
+ };
9
+ type WhereCallback<TShape extends ReadShape> = (q: FieldSet<TShape>) => AnyConstructable;
10
+ type OrderByCallback<TShape extends ReadShape> = (q: SortSet<ActionRecord<TShape>>) => ConstructableOrderBy | Array<ConstructableOrderBy>;
11
+ type ListBuilder<TShape extends ReadShape> = {
12
+ /**
13
+ * Narrows down which records come back.
14
+ *
15
+ * @example
16
+ * ds.list(posts).where((q) => q.title.contains("cafe"))
17
+ */
18
+ where(build: WhereCallback<TShape>): ListBuilder<TShape>;
19
+ /**
20
+ * Sorts the records.
21
+ *
22
+ * @example
23
+ * ds.list(posts).orderBy((q) => q.publishedAt.asc)
24
+ *
25
+ * An array can be provided to solve tie-breakers.
26
+ *
27
+ * @example
28
+ * ds.list(posts).orderBy((q) => [q.views.desc, q.publishedAt.asc])
29
+ */
30
+ orderBy(build: OrderByCallback<TShape>): ListBuilder<TShape>;
31
+ /** How many records to return. Defaults to 20, capped at 100. */
32
+ limit(count: number): ListBuilder<TShape>;
33
+ /**
34
+ * Where the page starts. Pass the `nextOffset` of a previous result, or `null` to start
35
+ * over. A result with `nextOffset: null` is the last page.
36
+ *
37
+ * @example
38
+ * const first = await ds.list(posts).limit(50)
39
+ * const second = await ds.list(posts).limit(50).offset(first.nextOffset)
40
+ */
41
+ offset(offset: number | null): ListBuilder<TShape>;
42
+ } & Promise<ListResult<SoftrRecord<ActionRecord<TShape>>>>;
43
+ /**
44
+ * One record, fetched when awaited. There is no `orderBy`, `limit` or `offset`. The record is
45
+ * picked by id, so there is nothing to order or page through.
46
+ *
47
+ * @example
48
+ * const post = await ds.get(postDetail, { id: params.postId })
49
+ */
50
+ type GetBuilder<TShape extends ReadShape> = {
51
+ /**
52
+ * Narrows which record the id may resolve to. A record that does not match the filter
53
+ * reads as `null`, the same as one that does not exist.
54
+ *
55
+ * @example
56
+ * const post = await ds
57
+ * .get(postDetail, { id: params.postId })
58
+ * .where((q) => q.author.is(user.id))
59
+ */
60
+ where(build: WhereCallback<TShape>): GetBuilder<TShape>;
61
+ } & Promise<SoftrRecord<ActionRecord<TShape>> | null>;
62
+ interface SoftrRecord<TFields extends Record<string, unknown> = Record<string, unknown>> {
63
+ id: string;
64
+ fields: TFields;
65
+ }
66
+ interface ListResult<TRecord extends SoftrRecord = SoftrRecord> {
67
+ records: TRecord[];
68
+ total: number;
69
+ nextOffset: number | null;
70
+ }
71
+ /**
72
+ * Queries a list of records from the datasource.
73
+ *
74
+ * @example
75
+ * const { records, total, nextOffset } = await ds
76
+ * .list(posts)
77
+ * .where((q) => q.publishedAt.lte(today))
78
+ * .orderBy((q) => q.publishedAt.desc)
79
+ * .limit(50)
80
+ */
81
+ declare function listRecords<TShape extends ReadShape>(query: Action<TShape>): ListBuilder<TShape>;
82
+ /**
83
+ * Gets a single record by ID.
84
+ *
85
+ * @example
86
+ * const record = await ds
87
+ * .get(posts, { id: "record-id" })
88
+ * .where((q) => q.publishedAt.lte(today))
89
+ *
90
+ * A filter may also be provided to narrow down
91
+ *
92
+ * @example
93
+ * const record = await ds
94
+ * .get(posts, { id: "record-id" })
95
+ * .where((q) => q.publishedAt.lte(today))
96
+ */
97
+ declare function getRecord<TShape extends ReadShape>(query: Action<TShape>, options: GetOptions): GetBuilder<TShape>;
98
+ /**
99
+ * Creates a record and returns it.
100
+ *
101
+ * @example
102
+ * const record = await ds.create(newPost, {
103
+ * data: { title: "Cafes", publishedAt: today },
104
+ * })
105
+ */
106
+ declare function createRecord<TShape extends ActionShape & {
107
+ operation: "create";
108
+ }>(action: Action<TShape>, options: {
109
+ data: ActionData<TShape>;
110
+ }): Promise<SoftrRecord<ActionRecord<TShape>>>;
111
+ /**
112
+ * Updates a record by ID and returns it. Only the fields in `data` change.
113
+ *
114
+ * @example
115
+ * const record = await ds.update(editPost, {
116
+ * id: "record-id",
117
+ * data: { title: "Cafes" },
118
+ * })
119
+ */
120
+ declare function updateRecord<TShape extends ActionShape & {
121
+ operation: "update";
122
+ }>(action: Action<TShape>, options: {
123
+ id: string;
124
+ data: ActionData<TShape>;
125
+ }): Promise<SoftrRecord<ActionRecord<TShape>>>;
126
+ /**
127
+ * Deletes a record by ID.
128
+ *
129
+ * @example
130
+ * await ds.delete(removePost, { id: "record-id" })
131
+ */
132
+ declare function deleteRecord<TShape extends ActionShape & {
133
+ operation: "delete";
134
+ }>(action: Action<TShape>, options: {
135
+ id: string;
136
+ }): Promise<void>;
137
+ //#endregion
138
+ export { WhereCallback, createRecord, deleteRecord, getRecord, listRecords, updateRecord };
@@ -0,0 +1,258 @@
1
+ import { assert, panic } from "../assertions.js";
2
+ import { isAction } from "../action.js";
3
+ import { readUserTimezone } from "../cookies.js";
4
+ import { readSessionToken } from "../auth/session.js";
5
+ import { buildFilterCriteria, buildOrderBy, dealiasFilterCriteria, dealiasOrderBy } from "./filter.js";
6
+ import { andAll, makeFieldSet, makeSortSet } from "./fields.js";
7
+
8
+ //#region src/datasource/record.ts
9
+ const MAX_RECORD_COUNT = 100;
10
+ const dsUrl = (datasourceId) => {
11
+ const datasourceApiUrl = process.env.SOFTR_DATASOURCE_API_URL;
12
+ if (!datasourceApiUrl) panic("SOFTR_DATASOURCE_API_URL is not set.");
13
+ const applicationId = process.env.SOFTR_APPLICATION_ID;
14
+ if (!applicationId) panic("SOFTR_APPLICATION_ID is not set.");
15
+ return `${withoutTrailingSlash(datasourceApiUrl)}/v1/datasource/vibe/applications/${applicationId}/datasources/${datasourceId}`;
16
+ };
17
+ /**
18
+ * Queries a list of records from the datasource.
19
+ *
20
+ * @example
21
+ * const { records, total, nextOffset } = await ds
22
+ * .list(posts)
23
+ * .where((q) => q.publishedAt.lte(today))
24
+ * .orderBy((q) => q.publishedAt.desc)
25
+ * .limit(50)
26
+ */
27
+ function listRecords(query) {
28
+ const aliases = Object.keys(resolveTarget(query, "read").select);
29
+ const args = {
30
+ filters: [],
31
+ orderBy: []
32
+ };
33
+ let page;
34
+ const run = () => page ??= executeList(query, args);
35
+ const builder = {
36
+ [Symbol.toStringTag]: "RecordListing",
37
+ where(build) {
38
+ args.filters.push(build(makeFieldSet(aliases)));
39
+ return builder;
40
+ },
41
+ orderBy(build) {
42
+ const sorting = build(makeSortSet(aliases));
43
+ args.orderBy.push(...Array.isArray(sorting) ? sorting : [sorting]);
44
+ return builder;
45
+ },
46
+ limit(count) {
47
+ args.limit = count;
48
+ return builder;
49
+ },
50
+ offset(offset) {
51
+ args.offset = offset;
52
+ return builder;
53
+ },
54
+ then(onfulfilled, onrejected) {
55
+ return run().then(onfulfilled, onrejected);
56
+ },
57
+ catch(onrejected) {
58
+ return run().catch(onrejected);
59
+ },
60
+ finally(onfinally) {
61
+ return run().finally(onfinally);
62
+ }
63
+ };
64
+ return builder;
65
+ }
66
+ async function executeList(query, args) {
67
+ const { datasourceId, select } = resolveTarget(query, "read");
68
+ const count = Math.min(args.limit ?? 20, MAX_RECORD_COUNT);
69
+ const offset = args.offset ?? null;
70
+ const filterCriteria = args.filters.length ? dealiasFilterCriteria(buildFilterCriteria(andAll(args.filters)), select) : {};
71
+ const sortingOption = args.orderBy.length ? dealiasOrderBy(buildOrderBy(args.orderBy), select) : void 0;
72
+ const res = await fetch(`${dsUrl(datasourceId)}/records`, {
73
+ method: "POST",
74
+ headers: {
75
+ "Content-Type": "application/json",
76
+ "X-User-Timezone": readUserTimezone() ?? "UTC",
77
+ Authorization: `Bearer ${readSessionToken()}`
78
+ },
79
+ body: JSON.stringify({
80
+ options: { userLocale: "en-US" },
81
+ filterCriteria,
82
+ sortingOption,
83
+ pagingOption: {
84
+ offset,
85
+ count
86
+ }
87
+ })
88
+ });
89
+ if (!res.ok) throw new Error(`Softr datasource list failed for "${datasourceId}": ${res.status}`);
90
+ const payload = await res.json();
91
+ const nextOffset = payload.offset < payload.total ? payload.offset : null;
92
+ return {
93
+ records: payload.items.map((item) => ({
94
+ id: item.id,
95
+ fields: aliasFields(item.fields, select)
96
+ })),
97
+ total: payload.total,
98
+ nextOffset
99
+ };
100
+ }
101
+ /**
102
+ * Gets a single record by ID.
103
+ *
104
+ * @example
105
+ * const record = await ds
106
+ * .get(posts, { id: "record-id" })
107
+ * .where((q) => q.publishedAt.lte(today))
108
+ *
109
+ * A filter may also be provided to narrow down
110
+ *
111
+ * @example
112
+ * const record = await ds
113
+ * .get(posts, { id: "record-id" })
114
+ * .where((q) => q.publishedAt.lte(today))
115
+ */
116
+ function getRecord(query, options) {
117
+ const aliases = Object.keys(resolveTarget(query, "read").select);
118
+ const filters = [];
119
+ let record;
120
+ const run = () => record ??= executeGet(query, options.id, filters);
121
+ const builder = {
122
+ [Symbol.toStringTag]: "RecordFetch",
123
+ where(build) {
124
+ filters.push(build(makeFieldSet(aliases)));
125
+ return builder;
126
+ },
127
+ then(onfulfilled, onrejected) {
128
+ return run().then(onfulfilled, onrejected);
129
+ },
130
+ catch(onrejected) {
131
+ return run().catch(onrejected);
132
+ },
133
+ finally(onfinally) {
134
+ return run().finally(onfinally);
135
+ }
136
+ };
137
+ return builder;
138
+ }
139
+ async function executeGet(query, id, filters) {
140
+ assert(id, "a record id must not be empty");
141
+ const { datasourceId, select } = resolveTarget(query, "read");
142
+ const filterCriteria = filters.length ? dealiasFilterCriteria(buildFilterCriteria(andAll(filters)), select) : {};
143
+ const res = await fetch(`${dsUrl(datasourceId)}/records/${id}`, {
144
+ method: "POST",
145
+ headers: {
146
+ "Content-Type": "application/json",
147
+ "X-User-Timezone": readUserTimezone() ?? "UTC",
148
+ Authorization: `Bearer ${readSessionToken()}`
149
+ },
150
+ body: JSON.stringify({
151
+ options: { userLocale: "en-US" },
152
+ filterCriteria
153
+ })
154
+ });
155
+ if (!res.ok) throw new Error(`Softr datasource get failed for "${datasourceId}": ${res.status}`);
156
+ const contentLength = res.headers.get("Content-Length");
157
+ if (contentLength && parseInt(contentLength, 10) === 0) return null;
158
+ const record = await res.json();
159
+ return {
160
+ id: record.id,
161
+ fields: aliasFields(record.fields, select)
162
+ };
163
+ }
164
+ /**
165
+ * Creates a record and returns it.
166
+ *
167
+ * @example
168
+ * const record = await ds.create(newPost, {
169
+ * data: { title: "Cafes", publishedAt: today },
170
+ * })
171
+ */
172
+ async function createRecord(action, options) {
173
+ const { datasourceId, select } = resolveTarget(action, "create");
174
+ const data = options.data;
175
+ const rawFields = dealiasData(data, select);
176
+ const res = await fetch(`${dsUrl(datasourceId)}/records/new`, {
177
+ method: "POST",
178
+ headers: {
179
+ "Content-Type": "application/json",
180
+ Authorization: `Bearer ${readSessionToken()}`
181
+ },
182
+ body: JSON.stringify({ fields: rawFields })
183
+ });
184
+ if (!res.ok) throw new Error(`Softr datasource create failed for "${datasourceId}": ${res.status}`);
185
+ const record = await res.json();
186
+ return {
187
+ id: record.id,
188
+ fields: aliasFields(record.fields, select)
189
+ };
190
+ }
191
+ /**
192
+ * Updates a record by ID and returns it. Only the fields in `data` change.
193
+ *
194
+ * @example
195
+ * const record = await ds.update(editPost, {
196
+ * id: "record-id",
197
+ * data: { title: "Cafes" },
198
+ * })
199
+ */
200
+ async function updateRecord(action, options) {
201
+ if (!options.id) throw new Error(`Softr datasource update: id must not be empty`);
202
+ const { datasourceId, select } = resolveTarget(action, "update");
203
+ const data = options.data;
204
+ const rawFields = dealiasData(data, select);
205
+ const res = await fetch(`${dsUrl(datasourceId)}/records/${options.id}`, {
206
+ method: "PATCH",
207
+ headers: {
208
+ "Content-Type": "application/json",
209
+ Authorization: `Bearer ${readSessionToken()}`
210
+ },
211
+ body: JSON.stringify({ fields: rawFields })
212
+ });
213
+ if (!res.ok) throw new Error(`Softr datasource update failed for "${datasourceId}": ${res.status}`);
214
+ const record = await res.json();
215
+ return {
216
+ id: record.id,
217
+ fields: aliasFields(record.fields, select)
218
+ };
219
+ }
220
+ /**
221
+ * Deletes a record by ID.
222
+ *
223
+ * @example
224
+ * await ds.delete(removePost, { id: "record-id" })
225
+ */
226
+ async function deleteRecord(action, options) {
227
+ const { datasourceId } = resolveTarget(action, "delete");
228
+ const res = await fetch(`${dsUrl(datasourceId)}/records/${options.id}`, {
229
+ method: "DELETE",
230
+ headers: { Authorization: `Bearer ${readSessionToken()}` }
231
+ });
232
+ if (!res.ok) throw new Error(`Softr datasource delete failed for "${datasourceId}": ${res.status}`);
233
+ }
234
+ function resolveTarget(subject, operation) {
235
+ assert(isAction(subject), "a read or a mutation takes an action from defineAction");
236
+ const action = subject;
237
+ assert(action.operation === operation, () => `action "${action.id}" is declared as ${action.operation}, not ${operation}`);
238
+ return {
239
+ datasourceId: action.datasource,
240
+ select: action.fields ?? {}
241
+ };
242
+ }
243
+ function aliasFields(fields, aliases) {
244
+ const result = {};
245
+ for (const [alias, fieldId] of Object.entries(aliases)) result[alias] = fields[fieldId];
246
+ return result;
247
+ }
248
+ function dealiasData(data, select) {
249
+ const result = {};
250
+ for (const [alias, value] of Object.entries(data)) result[select[alias] ?? alias] = value;
251
+ return result;
252
+ }
253
+ function withoutTrailingSlash(url) {
254
+ return url.replace(/\/+$/, "");
255
+ }
256
+
257
+ //#endregion
258
+ export { createRecord, deleteRecord, dsUrl, getRecord, listRecords, resolveTarget, updateRecord };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- import { q } from "./core/query.js";
2
1
  import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
3
- import { AggregationFunction, ChartOptions, ChartResult, CreateOptions, DatasourcePermissions, DatasourceTypeMap, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions } from "./types.js";
4
- import { Permissions } from "./auth/permissions.js";
2
+ import { SoftrDatasources, defineAction } from "./action.js";
5
3
  import { SoftrContext } from "./auth/context.js";
6
- import { AccessBehaviour, AccessCheck, AccessError, AccessReason, RouteVisibility, VisibilityHandle, defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
4
+ import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
7
5
  import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
8
6
  import { useUserTimezone } from "./timezone.js";
9
7
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
10
- export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type ChartOptions, type ChartResult, type CreateOptions, type DatasourcePermissions, type DatasourceTypeMap, DefaultErrorOverlay, type GetOptions, type JsonValue, type ListOptions, type ListResult, type MetricOptions, type MetricResult, type Permissions, type RecordFieldsFor, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrUser, type SoftrUserGroupId, type SoftrUserGroups, type UpdateOptions, type User, type VisibilityHandle, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
8
+ export { DefaultErrorOverlay, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, defineAction, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { q } from "./core/query.js";
1
+ import { defineAction } from "./action.js";
2
2
  import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
3
3
  import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
4
4
  import { useUserTimezone } from "./timezone.js";
5
5
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
6
6
 
7
- export { DefaultErrorOverlay, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
7
+ export { DefaultErrorOverlay, defineAction, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
@@ -0,0 +1,5 @@
1
+ //#region package.json
2
+ var version = "0.0.26";
3
+
4
+ //#endregion
5
+ export { version };