@softr/sdk 0.0.74-alpha.0 → 0.0.75

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 (47) hide show
  1. package/dist/action.d.ts +31 -1
  2. package/dist/action.js +26 -1
  3. package/dist/auth/methods/profile.d.ts +12 -3
  4. package/dist/auth/methods/profile.js +18 -4
  5. package/dist/auth/platform.js +4 -0
  6. package/dist/auth/server.d.ts +2 -1
  7. package/dist/auth/server.js +2 -1
  8. package/dist/client.d.ts +2 -1
  9. package/dist/client.js +2 -1
  10. package/dist/codegen/datasource-types.js +14 -1
  11. package/dist/comments/comments.d.ts +168 -0
  12. package/dist/comments/comments.js +413 -0
  13. package/dist/comments/use-comment-composer.d.ts +46 -0
  14. package/dist/comments/use-comment-composer.js +134 -0
  15. package/dist/core/file-upload.d.ts +7 -2
  16. package/dist/core/file-upload.js +6 -1
  17. package/dist/core/options.d.ts +2 -17
  18. package/dist/core/options.js +2 -30
  19. package/dist/core/use-file-upload.d.ts +2 -2
  20. package/dist/datasource/datasource.d.ts +1 -2
  21. package/dist/datasource/datasource.js +2 -3
  22. package/dist/datasource/fields.d.ts +24 -16
  23. package/dist/formatting.d.ts +3 -3
  24. package/dist/index.d.ts +3 -2
  25. package/dist/index.js +2 -2
  26. package/dist/mcp.d.ts +2 -2
  27. package/dist/package.js +1 -1
  28. package/dist/pwa.d.ts +2 -2
  29. package/dist/server.d.ts +3 -2
  30. package/dist/server.js +2 -1
  31. package/package.json +12 -33
  32. package/skills/softr-sdk/SKILL.md +8 -4
  33. package/skills/softr-sdk/auth/SKILL.md +4 -3
  34. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  35. package/skills/softr-sdk/comments/SKILL.md +181 -0
  36. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  37. package/skills/softr-sdk/file-uploads/SKILL.md +5 -4
  38. package/skills/softr-sdk/formatting/SKILL.md +1 -1
  39. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  40. package/skills/softr-sdk/mcp/SKILL.md +1 -1
  41. package/skills/softr-sdk/pwa/SKILL.md +1 -1
  42. package/skills/softr-sdk/record-mutations/SKILL.md +2 -2
  43. package/skills/softr-sdk/record-queries/SKILL.md +1 -7
  44. package/skills/softr-sdk/record-queries/references/filters-and-sorting.md +1 -1
  45. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  46. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  47. package/skills/softr-sdk/workflows/SKILL.md +1 -1
package/dist/action.d.ts CHANGED
@@ -234,5 +234,35 @@ type IntegrationDefinition<TId extends string> = {
234
234
  * const res = await listEvents({ query: { timeMin: now, singleEvents: "true" } })
235
235
  */
236
236
  declare function defineIntegration<const TId extends string>(definition: IntegrationDefinition<TId>): IntegrationAction<TId>;
237
+ type CommentsResource = {
238
+ type: "COMMENTS";
239
+ datasourceId: SoftrDatasourceId;
240
+ };
241
+ /** Comment threads on the records of one datasource. It has no `permissions`. */
242
+ type CommentsAction<TId extends string = string> = {
243
+ id: TId;
244
+ resource: CommentsResource;
245
+ };
246
+ type CommentsDefinition<TId extends string> = {
247
+ id: TId;
248
+ datasourceId: SoftrDatasourceId;
249
+ };
250
+ /**
251
+ * Declares comment threads on the records of a datasource. Each record has one thread.
252
+ *
253
+ * Who may read, post and delete comes from the app's comment settings in Softr, not from code. Two
254
+ * declarations on the same datasource share the same threads.
255
+ *
256
+ * Pass the returned object to `comments.*` from `@softr/sdk/server`.
257
+ *
258
+ * @example
259
+ * const orderComments = defineComments({
260
+ * id: "orderComments",
261
+ * datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
262
+ * })
263
+ *
264
+ * const thread = await comments.list(orderComments, { recordId })
265
+ */
266
+ declare function defineComments<const TId extends string>(definition: CommentsDefinition<TId>): CommentsAction<TId>;
237
267
  //#endregion
238
- export { ActionData, ActionPermissions, ActionRecord, ActionShape, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
268
+ export { ActionData, ActionPermissions, ActionRecord, ActionShape, CommentsAction, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow };
package/dist/action.js CHANGED
@@ -50,6 +50,31 @@ function defineIntegration(definition) {
50
50
  permissions
51
51
  };
52
52
  }
53
+ /**
54
+ * Declares comment threads on the records of a datasource. Each record has one thread.
55
+ *
56
+ * Who may read, post and delete comes from the app's comment settings in Softr, not from code. Two
57
+ * declarations on the same datasource share the same threads.
58
+ *
59
+ * Pass the returned object to `comments.*` from `@softr/sdk/server`.
60
+ *
61
+ * @example
62
+ * const orderComments = defineComments({
63
+ * id: "orderComments",
64
+ * datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
65
+ * })
66
+ *
67
+ * const thread = await comments.list(orderComments, { recordId })
68
+ */
69
+ function defineComments(definition) {
70
+ return {
71
+ id: definition.id,
72
+ resource: {
73
+ type: "COMMENTS",
74
+ datasourceId: definition.datasourceId
75
+ }
76
+ };
77
+ }
53
78
  function isDatasourceAction(value) {
54
79
  if (typeof value !== "object" || value === null) return false;
55
80
  const candidate = value;
@@ -58,4 +83,4 @@ function isDatasourceAction(value) {
58
83
  }
59
84
 
60
85
  //#endregion
61
- export { defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
86
+ export { defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
@@ -5,7 +5,7 @@ import { AuthError, Result } from "./utils.js";
5
5
 
6
6
  /**
7
7
  * Changes the signed-in visitor's own name, photo, or both. A field left out
8
- * stays as it is, and `avatar: null` removes the photo.
8
+ * stays as it is. {@link removeAvatar} removes the photo.
9
9
  *
10
10
  * The photo can change only where `user.avatarEditable` is true. Elsewhere a
11
11
  * photo answers `avatarNotSupported`, so hide the photo controls instead.
@@ -20,7 +20,16 @@ import { AuthError, Result } from "./utils.js";
20
20
  */
21
21
  declare function updateProfile(changes: {
22
22
  fullName?: string;
23
- avatar?: File | null;
23
+ avatar?: File;
24
24
  }): Promise<Result<User, AuthError>>;
25
+ /**
26
+ * Removes the signed-in visitor's own photo. Like {@link updateProfile}, it
27
+ * works only where `user.avatarEditable` is true, and answers with the visitor
28
+ * as they are now.
29
+ *
30
+ * @example
31
+ * const result = await auth.removeAvatar()
32
+ */
33
+ declare function removeAvatar(): Promise<Result<User, AuthError>>;
25
34
  //#endregion
26
- export { updateProfile };
35
+ export { removeAvatar, updateProfile };
@@ -7,7 +7,7 @@ import { authError } from "./utils.js";
7
7
  //#region src/auth/methods/profile.ts
8
8
  /**
9
9
  * Changes the signed-in visitor's own name, photo, or both. A field left out
10
- * stays as it is, and `avatar: null` removes the photo.
10
+ * stays as it is. {@link removeAvatar} removes the photo.
11
11
  *
12
12
  * The photo can change only where `user.avatarEditable` is true. Elsewhere a
13
13
  * photo answers `avatarNotSupported`, so hide the photo controls instead.
@@ -26,9 +26,23 @@ async function updateProfile(changes) {
26
26
  if (token === null) return authError("notAuthenticated", "Nobody is signed in.");
27
27
  const form = new FormData();
28
28
  if (changes.fullName !== void 0) form.set("fullName", changes.fullName);
29
- if (changes.avatar === null) form.set("removeAvatar", "true");
30
29
  if (changes.avatar) form.set("avatar", changes.avatar);
31
- const result = await makePlatform(sessionConfig().applicationId).updateProfile(token, form);
30
+ return answer(await makePlatform(sessionConfig().applicationId).updateProfile(token, form));
31
+ }
32
+ /**
33
+ * Removes the signed-in visitor's own photo. Like {@link updateProfile}, it
34
+ * works only where `user.avatarEditable` is true, and answers with the visitor
35
+ * as they are now.
36
+ *
37
+ * @example
38
+ * const result = await auth.removeAvatar()
39
+ */
40
+ async function removeAvatar() {
41
+ const token = readSessionToken();
42
+ if (token === null) return authError("notAuthenticated", "Nobody is signed in.");
43
+ return answer(await makePlatform(sessionConfig().applicationId).removeAvatar(token));
44
+ }
45
+ function answer(result) {
32
46
  if (!result.ok) {
33
47
  switch (result.status) {
34
48
  case 400: return authError("invalidName", result.message);
@@ -49,4 +63,4 @@ async function updateProfile(changes) {
49
63
  }
50
64
 
51
65
  //#endregion
52
- export { updateProfile };
66
+ export { removeAvatar, updateProfile };
@@ -272,6 +272,10 @@ function makePlatform(applicationId) {
272
272
  token,
273
273
  form
274
274
  }, userSchema),
275
+ removeAvatar: (token) => request(`/v1/vibe/applications/${applicationId}/users/me/profile/avatar`, {
276
+ method: "DELETE",
277
+ token
278
+ }, userSchema),
275
279
  signOut: (token) => request(`/v1/applications/${applicationId}/users/sign-out`, {
276
280
  method: "POST",
277
281
  token
@@ -6,7 +6,7 @@ import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./
6
6
  import { settings } from "./methods/settings.js";
7
7
  import { confirmSignUp, signUp } from "./methods/signup.js";
8
8
  import { signInWithGoogle } from "./methods/social.js";
9
- import { updateProfile } from "./methods/profile.js";
9
+ import { removeAvatar, updateProfile } from "./methods/profile.js";
10
10
  import { currentUserRow } from "./current-user-row.js";
11
11
  import "@tanstack/react-start/server-only";
12
12
 
@@ -36,6 +36,7 @@ declare const auth: {
36
36
  removeAuthenticator: typeof removeAuthenticator;
37
37
  settings: typeof settings;
38
38
  updateProfile: typeof updateProfile;
39
+ removeAvatar: typeof removeAvatar;
39
40
  currentUserRow: typeof currentUserRow;
40
41
  };
41
42
  //#endregion
@@ -6,7 +6,7 @@ import { signIn, signOut } from "./methods/password.js";
6
6
  import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./methods/passwordLink.js";
7
7
  import { confirmSignUp, signUp } from "./methods/signup.js";
8
8
  import { signInWithGoogle } from "./methods/social.js";
9
- import { updateProfile } from "./methods/profile.js";
9
+ import { removeAvatar, updateProfile } from "./methods/profile.js";
10
10
  import { currentUserRow } from "./current-user-row.js";
11
11
  import "@tanstack/react-start/server-only";
12
12
 
@@ -35,6 +35,7 @@ const auth = {
35
35
  removeAuthenticator,
36
36
  settings,
37
37
  updateProfile,
38
+ removeAvatar,
38
39
  currentUserRow
39
40
  };
40
41
 
package/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { FileItem, useFileUpload } from "./core/use-file-upload.js";
2
+ import { useCommentComposer } from "./comments/use-comment-composer.js";
2
3
  import "@tanstack/react-start/client-only";
3
- export { type FileItem, useFileUpload };
4
+ export { type FileItem, useCommentComposer, useFileUpload };
package/dist/client.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { useFileUpload } from "./core/use-file-upload.js";
2
+ import { useCommentComposer } from "./comments/use-comment-composer.js";
2
3
  import "@tanstack/react-start/client-only";
3
4
 
4
- export { useFileUpload };
5
+ export { useCommentComposer, useFileUpload };
@@ -161,7 +161,7 @@ function writeValue(shape, context) {
161
161
  case "UPDATED_AT": return "IsoDateTime";
162
162
  case "TIME": return "IsoTime";
163
163
  case "DATE_RANGE": return "{ from: IsoDate; to: IsoDate }";
164
- case "SELECT": return "SelectChoiceInput";
164
+ case "SELECT": return selectInput(shape.options);
165
165
  case "ATTACHMENT": return "AttachmentInput";
166
166
  case "LINKED_RECORD": return "LinkedRecordInput";
167
167
  case "USER": return "WorkspaceUser";
@@ -208,6 +208,19 @@ function selectValue(options) {
208
208
  if (choices === null) return "SelectChoice";
209
209
  return union(choices.map(({ id, label }) => `{ id: ${quote(id)}; label: ${quote(label)} }`));
210
210
  }
211
+ /**
212
+ * A write carries the id alone. The API resolves it against the choice list and writes that
213
+ * choice's own label, so a label sent alongside is dropped.
214
+ *
215
+ * The API also takes `{ label }` with no id, and a label matching nothing creates a choice.
216
+ * The union omits that: it would make every misspelling type-check, and the cost of one is a
217
+ * new choice in the field rather than an error.
218
+ */
219
+ function selectInput(options) {
220
+ const choices = choiceEntries(options);
221
+ if (choices === null) return "SelectChoiceInput";
222
+ return union(choices.map(({ id }) => `{ id: ${quote(id)} }`));
223
+ }
211
224
  function datePrecision(options) {
212
225
  const precision = option(options, "precision");
213
226
  return typeof precision === "string" ? precision : null;
@@ -0,0 +1,168 @@
1
+ import { CommentsAction } from "../action.js";
2
+
3
+ //#region src/comments/comments.d.ts
4
+ /** A person picked in the composer, and where their `@name` sits in the text. */
5
+ type CommentMention = {
6
+ name: string;
7
+ email: string;
8
+ start: number;
9
+ end: number;
10
+ };
11
+ type CommentAttachmentInput = {
12
+ url: string;
13
+ filename: string;
14
+ type?: string | null;
15
+ };
16
+ type CommentView = {
17
+ id: string;
18
+ /** The body as safe HTML. Render it with `dangerouslySetInnerHTML`, never as text. */
19
+ bodyHtml: string;
20
+ attachments: Array<{
21
+ url: string;
22
+ filename: string;
23
+ type: string | null;
24
+ }>;
25
+ author: {
26
+ name: string;
27
+ avatar: string | null;
28
+ isDeactivated: boolean;
29
+ };
30
+ /** ISO 8601. */
31
+ createdAt: string;
32
+ canDelete: boolean;
33
+ canEdit: boolean;
34
+ edited: boolean;
35
+ /** The body as the visitor typed it, to fill an edit box. `null` unless `canEdit`. */
36
+ source: {
37
+ text: string;
38
+ mentions: Array<CommentMention>;
39
+ } | null;
40
+ reactions: Array<{
41
+ emoji: string;
42
+ label: string;
43
+ count: number;
44
+ isSelected: boolean;
45
+ }>;
46
+ };
47
+ type CommentThread = {
48
+ canPost: boolean;
49
+ allowMentions: boolean;
50
+ allowAttachments: boolean;
51
+ notificationsEnabled: boolean;
52
+ quickReactions: ReadonlyArray<{
53
+ emoji: string;
54
+ label: string;
55
+ }>;
56
+ total: number;
57
+ /** Pass it back as `offset` for the next page. `null` on the last page. */
58
+ nextOffset: number | null;
59
+ items: Array<CommentView>;
60
+ };
61
+ /**
62
+ * The thread on one record, or `null` when this visitor cannot see comments on it. Render nothing
63
+ * for `null`.
64
+ *
65
+ * @example
66
+ * const thread = await comments.list(orderComments, { recordId: order.id })
67
+ */
68
+ declare function listComments(action: CommentsAction, options: {
69
+ recordId: string;
70
+ offset?: number | null;
71
+ limit?: number;
72
+ }): Promise<CommentThread | null>;
73
+ /**
74
+ * Posts a comment. `body` is the text the visitor typed. `mentions` are the people they picked,
75
+ * each with the range of its `@name` in `body`. A range that no longer reads as `@name` is posted as
76
+ * plain text. `attachments` are the files `useFileUpload` finished uploading.
77
+ *
78
+ * @example
79
+ * await comments.create(orderComments, { recordId, body: "Hey @Ann", mentions, attachments })
80
+ */
81
+ declare function createComment(action: CommentsAction, options: {
82
+ recordId: string;
83
+ body: string;
84
+ mentions: ReadonlyArray<CommentMention>;
85
+ attachments?: ReadonlyArray<CommentAttachmentInput>;
86
+ }): Promise<void>;
87
+ /**
88
+ * Saves an edit. Offer it only where the comment's `canEdit` is true, and fill the edit box from its
89
+ * `source`. `attachments` is the full list after the edit: the files kept plus any new ones.
90
+ *
91
+ * @example
92
+ * await comments.update(orderComments, { recordId, commentId, body, mentions, attachments })
93
+ */
94
+ declare function updateComment(action: CommentsAction, options: {
95
+ recordId: string;
96
+ commentId: string;
97
+ body: string;
98
+ mentions: ReadonlyArray<CommentMention>;
99
+ attachments: ReadonlyArray<CommentAttachmentInput>;
100
+ }): Promise<void>;
101
+ /**
102
+ * Deletes a comment. Offer it only where the comment's `canDelete` is true.
103
+ *
104
+ * @example
105
+ * await comments.delete(orderComments, { recordId, commentId })
106
+ */
107
+ declare function deleteComment(action: CommentsAction, options: {
108
+ recordId: string;
109
+ commentId: string;
110
+ }): Promise<void>;
111
+ /**
112
+ * Adds the visitor's reaction when `selected` is true, and removes it when false. Pass the
113
+ * reaction's current `isSelected` negated.
114
+ *
115
+ * @example
116
+ * await comments.react(orderComments, { recordId, commentId, emoji: "👍", selected: true })
117
+ */
118
+ declare function reactToComment(action: CommentsAction, options: {
119
+ recordId: string;
120
+ commentId: string;
121
+ emoji: string;
122
+ selected: boolean;
123
+ }): Promise<void>;
124
+ /**
125
+ * Whether the visitor gets an email when someone comments on this record.
126
+ *
127
+ * @example
128
+ * const subscribed = await comments.subscription(orderComments, { recordId })
129
+ */
130
+ declare function readSubscription(action: CommentsAction, options: {
131
+ recordId: string;
132
+ }): Promise<boolean>;
133
+ /**
134
+ * Turns the visitor's emails about this record's comments on or off.
135
+ *
136
+ * @example
137
+ * await comments.setSubscription(orderComments, { recordId, subscribed: false })
138
+ */
139
+ declare function setSubscription(action: CommentsAction, options: {
140
+ recordId: string;
141
+ subscribed: boolean;
142
+ }): Promise<void>;
143
+ /**
144
+ * The people the visitor may mention whose name or email matches `query`. Only call it when the
145
+ * thread's `allowMentions` is true.
146
+ *
147
+ * @example
148
+ * const people = await comments.searchMentions(orderComments, { query: "an" })
149
+ */
150
+ declare function searchMentions(action: CommentsAction, options: {
151
+ query: string;
152
+ }): Promise<Array<{
153
+ name: string;
154
+ email: string;
155
+ avatar: string | null;
156
+ }>>;
157
+ declare const comments: {
158
+ list: typeof listComments;
159
+ create: typeof createComment;
160
+ update: typeof updateComment;
161
+ delete: typeof deleteComment;
162
+ react: typeof reactToComment;
163
+ subscription: typeof readSubscription;
164
+ setSubscription: typeof setSubscription;
165
+ searchMentions: typeof searchMentions;
166
+ };
167
+ //#endregion
168
+ export { CommentAttachmentInput, CommentMention, CommentThread, CommentView, comments };