@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.
- package/dist/action.d.ts +31 -1
- package/dist/action.js +26 -1
- package/dist/auth/methods/profile.d.ts +12 -3
- package/dist/auth/methods/profile.js +18 -4
- package/dist/auth/platform.js +4 -0
- package/dist/auth/server.d.ts +2 -1
- package/dist/auth/server.js +2 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/codegen/datasource-types.js +14 -1
- package/dist/comments/comments.d.ts +168 -0
- package/dist/comments/comments.js +413 -0
- package/dist/comments/use-comment-composer.d.ts +46 -0
- package/dist/comments/use-comment-composer.js +134 -0
- package/dist/core/file-upload.d.ts +7 -2
- package/dist/core/file-upload.js +6 -1
- package/dist/core/options.d.ts +2 -17
- package/dist/core/options.js +2 -30
- package/dist/core/use-file-upload.d.ts +2 -2
- package/dist/datasource/datasource.d.ts +1 -2
- package/dist/datasource/datasource.js +2 -3
- package/dist/datasource/fields.d.ts +24 -16
- package/dist/formatting.d.ts +3 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/package.js +1 -1
- package/dist/pwa.d.ts +2 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +2 -1
- package/package.json +12 -33
- package/skills/softr-sdk/SKILL.md +8 -4
- package/skills/softr-sdk/auth/SKILL.md +4 -3
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/comments/SKILL.md +181 -0
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +5 -4
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +2 -2
- package/skills/softr-sdk/record-queries/SKILL.md +1 -7
- package/skills/softr-sdk/record-queries/references/filters-and-sorting.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
package/dist/core/options.js
CHANGED
|
@@ -1,37 +1,9 @@
|
|
|
1
|
-
import { assert } from "../assertions.js";
|
|
2
|
-
import { fetch } from "../fetch.js";
|
|
3
|
-
import { readSessionToken } from "../auth/session.js";
|
|
4
1
|
import { makeChain } from "../datasource/fields.js";
|
|
5
|
-
import {
|
|
2
|
+
import { listRecords } from "../datasource/record.js";
|
|
6
3
|
import "@tanstack/react-start/server-only";
|
|
7
4
|
|
|
8
5
|
//#region src/core/options.ts
|
|
9
6
|
/**
|
|
10
|
-
* The choices a SELECT field holds right now, read from Softr on every call. A choice the
|
|
11
|
-
* builder adds or renames later shows up without a new build. The field must be one the
|
|
12
|
-
* action declares. Any query or mutation over the table works.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* const loadStatuses = createServerFn().handler(() => ds.choices(listProjects, 'status'))
|
|
16
|
-
*/
|
|
17
|
-
async function listChoices(action, field) {
|
|
18
|
-
const { actionId, select } = resolveTarget(action, action.resource.operation);
|
|
19
|
-
const fieldId = select[field];
|
|
20
|
-
assert(fieldId, () => `action "${actionId}" declares no field "${field}"`);
|
|
21
|
-
const res = await fetch(`${dsUrl(actionId)}/metadata`, { headers: {
|
|
22
|
-
Authorization: `Bearer ${readSessionToken()}`,
|
|
23
|
-
...refererHeader()
|
|
24
|
-
} });
|
|
25
|
-
if (!res.ok) throw new Error(`datasource metadata failed for "${actionId}": ${await describeFailure(res)}`);
|
|
26
|
-
const metadataField = (await res.json()).fields.find(({ id }) => id === fieldId);
|
|
27
|
-
assert(metadataField?.type === "SELECT", () => `"${field}" on action "${actionId}" is not a SELECT field`);
|
|
28
|
-
return (metadataField.options?.choices ?? []).map(({ id, label, color }) => ({
|
|
29
|
-
id,
|
|
30
|
-
label,
|
|
31
|
-
color: color ?? null
|
|
32
|
-
}));
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
7
|
* The rows a linked field can point at, for a picker: each record of the query as its id and the text
|
|
36
8
|
* of one field, searched on that field when a term is given, a page at a time.
|
|
37
9
|
*
|
|
@@ -64,4 +36,4 @@ function labelOf(value) {
|
|
|
64
36
|
}
|
|
65
37
|
|
|
66
38
|
//#endregion
|
|
67
|
-
export {
|
|
39
|
+
export { listOptions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DatasourceAction } from "../action.js";
|
|
1
|
+
import { CommentsAction, DatasourceAction } from "../action.js";
|
|
2
2
|
import { FileAttachment } from "./file-upload.js";
|
|
3
3
|
|
|
4
4
|
//#region src/core/use-file-upload.d.ts
|
|
@@ -16,7 +16,7 @@ type FileItem = {
|
|
|
16
16
|
filename: string;
|
|
17
17
|
error: Error;
|
|
18
18
|
};
|
|
19
|
-
declare function useFileUpload(action: DatasourceAction, options?: {
|
|
19
|
+
declare function useFileUpload(action: DatasourceAction | CommentsAction, options?: {
|
|
20
20
|
initialAttachments?: FileAttachment[];
|
|
21
21
|
}): {
|
|
22
22
|
files: FileItem[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
|
|
2
|
-
import {
|
|
2
|
+
import { listOptions } from "../core/options.js";
|
|
3
3
|
import { chart, metric } from "./chart.js";
|
|
4
4
|
import "@tanstack/react-start/server-only";
|
|
5
5
|
|
|
@@ -13,7 +13,6 @@ declare const ds: {
|
|
|
13
13
|
metric: typeof metric;
|
|
14
14
|
chart: typeof chart;
|
|
15
15
|
options: typeof listOptions;
|
|
16
|
-
choices: typeof listChoices;
|
|
17
16
|
};
|
|
18
17
|
//#endregion
|
|
19
18
|
export { ds };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
|
|
2
2
|
import { chart, metric } from "./chart.js";
|
|
3
|
-
import {
|
|
3
|
+
import { listOptions } from "../core/options.js";
|
|
4
4
|
import "@tanstack/react-start/server-only";
|
|
5
5
|
|
|
6
6
|
//#region src/datasource/datasource.ts
|
|
@@ -12,8 +12,7 @@ const ds = {
|
|
|
12
12
|
delete: deleteRecord,
|
|
13
13
|
metric,
|
|
14
14
|
chart,
|
|
15
|
-
options: listOptions
|
|
16
|
-
choices: listChoices
|
|
15
|
+
options: listOptions
|
|
17
16
|
};
|
|
18
17
|
|
|
19
18
|
//#endregion
|
|
@@ -19,21 +19,17 @@ type NumberChain = EmptyChain & {
|
|
|
19
19
|
lte(value: number): ConstructableFilter;
|
|
20
20
|
between(start: number, end: number): ConstructableFilter;
|
|
21
21
|
};
|
|
22
|
-
type ArrayChain = EmptyChain & {
|
|
23
|
-
is(values: Array<
|
|
24
|
-
isOneOf(values: Array<
|
|
25
|
-
isNoneOf(values: Array<
|
|
26
|
-
hasAllOf(values: Array<
|
|
22
|
+
type ArrayChain<TMember$1 extends string> = EmptyChain & {
|
|
23
|
+
is(values: Array<TMember$1>): ConstructableFilter;
|
|
24
|
+
isOneOf(values: Array<TMember$1>): ConstructableFilter;
|
|
25
|
+
isNoneOf(values: Array<TMember$1>): ConstructableFilter;
|
|
26
|
+
hasAllOf(values: Array<TMember$1>): ConstructableFilter;
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
is(value: string): ConstructableFilter;
|
|
34
|
-
isNot(value: string): ConstructableFilter;
|
|
35
|
-
isOneOf(values: Array<string>): ConstructableFilter;
|
|
36
|
-
isNoneOf(values: Array<string>): ConstructableFilter;
|
|
28
|
+
type ChoiceChain<TId$1 extends string> = EmptyChain & {
|
|
29
|
+
is(value: TId$1): ConstructableFilter;
|
|
30
|
+
isNot(value: TId$1): ConstructableFilter;
|
|
31
|
+
isOneOf(values: Array<TId$1>): ConstructableFilter;
|
|
32
|
+
isNoneOf(values: Array<TId$1>): ConstructableFilter;
|
|
37
33
|
};
|
|
38
34
|
/** What DSv2 filters generically: matched whole or scanned, never against a set. */
|
|
39
35
|
type GenericChain = EmptyChain & {
|
|
@@ -77,20 +73,32 @@ type AnyChain = EmptyChain & {
|
|
|
77
73
|
between(start: number | string, end: number | string): ConstructableFilter;
|
|
78
74
|
isNotBetween(start: number | string, end: number | string): ConstructableFilter;
|
|
79
75
|
};
|
|
76
|
+
type IdOf<TValue> = TValue extends {
|
|
77
|
+
id: infer TId extends string;
|
|
78
|
+
} ? TId : string;
|
|
79
|
+
/**
|
|
80
|
+
* What a choice-shaped field compares against: its own ids where codegen read the choice
|
|
81
|
+
* list, and any string where it did not. A linked record is the open case, and it is the
|
|
82
|
+
* only thing distinguishing one from a select at the type level.
|
|
83
|
+
*/
|
|
84
|
+
type ChoiceIds<TValue> = string extends IdOf<TValue> ? string : IdOf<TValue>;
|
|
80
85
|
type TextFieldType = 'SINGLE_LINE_TEXT' | 'LONG_TEXT' | 'URL' | 'EMAIL' | 'PHONE' | 'ADDRESS' | 'RECORD_ID' | 'LOCATION' | 'TIME' | 'BUTTON';
|
|
81
86
|
type NumberFieldType = 'NUMBER' | 'PERCENT' | 'CURRENCY' | 'PROGRESS' | 'RATING' | 'DURATION' | 'COUNT' | 'AUTONUMBER';
|
|
82
87
|
type DateFieldType = 'DATE' | 'DATETIME' | 'CREATED_AT' | 'UPDATED_AT';
|
|
83
88
|
type ChoiceFieldType = 'SELECT' | 'LINKED_RECORD' | 'CREATED_BY' | 'UPDATED_BY';
|
|
84
89
|
/** The types with no single value to compare: only whether the field holds one. */
|
|
85
90
|
type EmptyFieldType = 'ATTACHMENT' | 'DATE_RANGE';
|
|
91
|
+
type ElementOf<TValue> = TValue extends Array<infer TMember> ? TMember : TValue;
|
|
86
92
|
/**
|
|
87
93
|
* The operators one alias offers, chosen from the type codegen declared for the field rather
|
|
88
94
|
* than from what it reads as: a `DATETIME` and a text field both read as `string`, and only
|
|
89
95
|
* the declared type tells them apart.
|
|
90
96
|
*
|
|
91
|
-
*
|
|
97
|
+
* `TValue` is still needed for the literal choice ids, which live on the read type and
|
|
98
|
+
* nowhere else. A field with no declared type lands on the permissive chain rather than on
|
|
99
|
+
* none.
|
|
92
100
|
*/
|
|
93
|
-
type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
|
|
101
|
+
type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain<ChoiceIds<ElementOf<TValue>>> : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain<ChoiceIds<TValue>> : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
|
|
94
102
|
type Combinators = {
|
|
95
103
|
$and(...members: Array<AnyConstructable>): ConstructableFilterGroup;
|
|
96
104
|
$or(...members: Array<AnyConstructable>): ConstructableFilterGroup;
|
package/dist/formatting.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react0 from "react";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
@@ -94,7 +94,7 @@ declare function FormattingScope({
|
|
|
94
94
|
/** A BCP 47 locale, such as `de-DE`. It drops the app's fixed date format inside the scope. */
|
|
95
95
|
locale?: string;
|
|
96
96
|
children?: ReactNode;
|
|
97
|
-
}):
|
|
97
|
+
}): react0.FunctionComponentElement<react0.ProviderProps<Scope>>;
|
|
98
98
|
/**
|
|
99
99
|
* A date in the app's format, safe to render on the server. A timestamp whose
|
|
100
100
|
* zone is not known yet renders `fallback` until the browser supplies it, so
|
|
@@ -115,6 +115,6 @@ declare function FormattedDate({
|
|
|
115
115
|
time?: boolean;
|
|
116
116
|
/** What renders while the visitor's zone is unknown. */
|
|
117
117
|
fallback?: ReactNode;
|
|
118
|
-
}):
|
|
118
|
+
}): react0.FunctionComponentElement<react0.FragmentProps>;
|
|
119
119
|
//#endregion
|
|
120
120
|
export { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
2
|
-
import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
|
+
import { ActionPermissions, SoftrDatasources, defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { FileAttachment } from "./core/file-upload.js";
|
|
4
4
|
import { FileItem, useFileUpload } from "./core/use-file-upload.js";
|
|
5
|
+
import { CommentAttachmentInput, CommentMention, CommentThread, CommentView } from "./comments/comments.js";
|
|
5
6
|
import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
|
|
6
7
|
import { SoftrContext } from "./auth/context.js";
|
|
7
8
|
import { AuthSettings } from "./auth/platform.js";
|
|
@@ -17,4 +18,4 @@ import { useUserTimezone } from "./timezone.js";
|
|
|
17
18
|
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
18
19
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
19
20
|
import { Audience } from "./route/audience.js";
|
|
20
|
-
export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
21
|
+
export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type CommentAttachmentInput, type CommentMention, type CommentThread, type CommentView, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineComments, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useFileUpload } from "./core/use-file-upload.js";
|
|
2
|
-
import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
|
+
import { defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
|
|
4
4
|
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
|
|
5
5
|
import { definePage, pageHead } from "./route/page.js";
|
|
@@ -14,4 +14,4 @@ import { useUserTimezone } from "./timezone.js";
|
|
|
14
14
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
15
15
|
import { Audience } from "./route/audience.js";
|
|
16
16
|
|
|
17
|
-
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
17
|
+
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineComments, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatasourceAction } from "./action.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as _tanstack_react_start5 from "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/mcp.d.ts
|
|
5
5
|
|
|
@@ -31,6 +31,6 @@ type SoftrMcpOptions = {
|
|
|
31
31
|
* ],
|
|
32
32
|
* }))
|
|
33
33
|
*/
|
|
34
|
-
declare function softrMcpMiddleware(options?: SoftrMcpOptions):
|
|
34
|
+
declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start5.RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
35
35
|
//#endregion
|
|
36
36
|
export { softrMcpMiddleware };
|
package/dist/package.js
CHANGED
package/dist/pwa.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react4 from "react";
|
|
2
2
|
import "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/pwa.d.ts
|
|
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
|
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
25
|
*/
|
|
26
|
-
declare function SoftrPwaHead():
|
|
26
|
+
declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
|
|
27
27
|
type PwaPlatform = "ios" | "android" | "desktop";
|
|
28
28
|
type PwaInstall = {
|
|
29
29
|
available: false;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { upload } from "./core/file-upload.js";
|
|
2
|
+
import { comments } from "./comments/comments.js";
|
|
2
3
|
import { auth } from "./auth/server.js";
|
|
3
|
-
import {
|
|
4
|
+
import { RecordOption, RecordOptions } from "./core/options.js";
|
|
4
5
|
import { ds } from "./datasource/datasource.js";
|
|
5
6
|
import { workflow } from "./core/workflow.js";
|
|
6
7
|
import { integration } from "./core/integration.js";
|
|
7
8
|
import "@tanstack/react-start/server-only";
|
|
8
|
-
export { type
|
|
9
|
+
export { type RecordOption, type RecordOptions, auth, comments, ds, integration, upload, workflow };
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ds } from "./datasource/datasource.js";
|
|
|
3
3
|
import { auth } from "./auth/server.js";
|
|
4
4
|
import { workflow } from "./core/workflow.js";
|
|
5
5
|
import { integration } from "./core/integration.js";
|
|
6
|
+
import { comments } from "./comments/comments.js";
|
|
6
7
|
import "@tanstack/react-start/server-only";
|
|
7
8
|
|
|
8
|
-
export { auth, ds, integration, upload, workflow };
|
|
9
|
+
export { auth, comments, ds, integration, upload, workflow };
|
package/package.json
CHANGED
|
@@ -1,43 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softr/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.75",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"./server": {
|
|
15
|
-
"types": "./dist/server.d.ts",
|
|
16
|
-
"import": "./dist/server.js"
|
|
17
|
-
},
|
|
18
|
-
"./vite": {
|
|
19
|
-
"types": "./dist/vite.d.ts",
|
|
20
|
-
"import": "./dist/vite.js"
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
"bin": {
|
|
24
|
-
"softr-sdk": "./dist/cli.js"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"dist",
|
|
28
|
-
"schema",
|
|
29
|
-
"skills",
|
|
30
|
-
"!skills/_artifacts"
|
|
31
|
-
],
|
|
32
|
-
"publishConfig": {
|
|
33
|
-
"access": "public"
|
|
34
|
-
},
|
|
6
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
7
|
+
"./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
|
|
8
|
+
"./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
|
|
9
|
+
"./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
|
|
10
|
+
},
|
|
11
|
+
"bin": { "softr-sdk": "./dist/cli.js" },
|
|
12
|
+
"files": ["dist", "schema", "skills", "!skills/_artifacts"],
|
|
13
|
+
"publishConfig": { "access": "public" },
|
|
35
14
|
"scripts": {
|
|
36
15
|
"build": "tsdown",
|
|
37
16
|
"test": "node --import ./src/test-utils/setup.ts --test",
|
|
38
17
|
"typecheck": "tsc --noEmit"
|
|
39
18
|
},
|
|
40
19
|
"dependencies": {
|
|
20
|
+
"marked": "^18.0.14",
|
|
21
|
+
"xss": "^1.0.15",
|
|
41
22
|
"zod": "^4.4.3"
|
|
42
23
|
},
|
|
43
24
|
"devDependencies": {
|
|
@@ -63,9 +44,7 @@
|
|
|
63
44
|
"optional": true
|
|
64
45
|
}
|
|
65
46
|
},
|
|
66
|
-
"keywords": [
|
|
67
|
-
"tanstack-intent"
|
|
68
|
-
],
|
|
47
|
+
"keywords": ["tanstack-intent"],
|
|
69
48
|
"intent": {
|
|
70
49
|
"version": 1,
|
|
71
50
|
"repo": "@softr/sdk",
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: core
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.75
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# Softr SDK core
|
|
@@ -30,9 +30,9 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
30
30
|
|
|
31
31
|
| Import | Runs | Holds |
|
|
32
32
|
| --- | --- | --- |
|
|
33
|
-
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
34
|
-
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
|
|
35
|
-
| `@softr/sdk/client` | Browser only |
|
|
33
|
+
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineComments`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
34
|
+
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration`, `comments` |
|
|
35
|
+
| `@softr/sdk/client` | Browser only | `useFileUpload`, `useCommentComposer` |
|
|
36
36
|
| `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
|
|
37
37
|
|
|
38
38
|
## Sub-Skills
|
|
@@ -51,6 +51,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
51
51
|
| Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
|
|
52
52
|
| Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
|
|
53
53
|
| Show a date, a time or a number | [formatting/SKILL.md](./formatting/SKILL.md) |
|
|
54
|
+
| Add comments to a record | [comments/SKILL.md](./comments/SKILL.md) |
|
|
54
55
|
|
|
55
56
|
## Quick Decision Tree
|
|
56
57
|
|
|
@@ -91,6 +92,9 @@ Adding an install button, or making the app installable?
|
|
|
91
92
|
Showing a date, a time or a number on a page?
|
|
92
93
|
load softr-sdk/formatting
|
|
93
94
|
|
|
95
|
+
Letting visitors comment on a record?
|
|
96
|
+
load softr-sdk/comments
|
|
97
|
+
|
|
94
98
|
Writing a server function, loader, or middleware in general?
|
|
95
99
|
not a Softr concern. Use the TanStack Start skills.
|
|
96
100
|
```
|
|
@@ -18,7 +18,7 @@ description: >-
|
|
|
18
18
|
metadata:
|
|
19
19
|
type: sub-skill
|
|
20
20
|
library: softr-sdk
|
|
21
|
-
library_version: 0.0.
|
|
21
|
+
library_version: 0.0.75
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -85,7 +85,8 @@ Changing the visitor's own profile:
|
|
|
85
85
|
|
|
86
86
|
| Call | Does |
|
|
87
87
|
| --- | --- |
|
|
88
|
-
| `auth.updateProfile({ fullName?, avatar? })` | Changes their name, their photo, or both
|
|
88
|
+
| `auth.updateProfile({ fullName?, avatar? })` | Changes their name, their photo, or both |
|
|
89
|
+
| `auth.removeAvatar()` | Removes their photo |
|
|
89
90
|
|
|
90
91
|
## Setup
|
|
91
92
|
|
|
@@ -461,7 +462,7 @@ After a reset, enrol the new app with `startAuthenticatorSetup` and `confirmAuth
|
|
|
461
462
|
```ts
|
|
462
463
|
const result = await auth.updateProfile({ fullName: 'Ann Lee' })
|
|
463
464
|
const result = await auth.updateProfile({ avatar: file }) // a File from an <input type="file">
|
|
464
|
-
const result = await auth.
|
|
465
|
+
const result = await auth.removeAvatar() // removes the photo
|
|
465
466
|
```
|
|
466
467
|
|
|
467
468
|
It answers with the updated `User`. Pass the file through a server function that takes `FormData`. The photo must be a PNG, JPEG or GIF of at most 5 MB, or it answers `notAnImage` or `fileTooLarge`. A blank or unacceptable name answers `invalidName`.
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.75
|
|
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.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: comments
|
|
3
|
+
description: >-
|
|
4
|
+
Comment threads on records: defineComments from @softr/sdk declares which
|
|
5
|
+
datasource has comments; comments.list, create, update, delete, react,
|
|
6
|
+
subscription, setSubscription and searchMentions from @softr/sdk/server read
|
|
7
|
+
and write one record's thread inside a server function; useCommentComposer
|
|
8
|
+
and useFileUpload from @softr/sdk/client keep a textarea's text, its
|
|
9
|
+
@mentions and its attachments in step. Bodies come back as sanitised
|
|
10
|
+
bodyHtml. A null thread means render nothing.
|
|
11
|
+
metadata:
|
|
12
|
+
type: sub-skill
|
|
13
|
+
library: softr-sdk
|
|
14
|
+
library_version: 0.0.75
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
**Boundary.** `comments` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler. Importing it into a component fails the build. `useCommentComposer` lives on `@softr/sdk/client` and runs in components. `defineComments` comes from `@softr/sdk` and can be imported anywhere.
|
|
18
|
+
|
|
19
|
+
# Comments on records
|
|
20
|
+
|
|
21
|
+
Each record of a commented datasource has one thread. The SDK reads and writes it. The app owns what you see: layout, copy, order, dates.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
27
|
+
import { defineComments, type CommentMention, type CommentAttachmentInput } from '@softr/sdk'
|
|
28
|
+
import { comments } from '@softr/sdk/server'
|
|
29
|
+
import { useCommentComposer, useFileUpload } from '@softr/sdk/client'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Declare the comments action
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export const orderComments = defineComments({
|
|
36
|
+
id: 'orderComments',
|
|
37
|
+
datasourceId: '8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77',
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `id` is your name for it, unique in the app. `datasourceId` is the table that gets threads.
|
|
42
|
+
- No `permissions`. Who can read, post and delete is set in Settings → Comments, shared with native blocks.
|
|
43
|
+
- A viewer sees a thread only if a `read` action (`defineQuery`) on the same table lets them read it. Without one, `list` returns `null` for them.
|
|
44
|
+
- One declaration per datasource. Two on the same table are the **same** threads, so "internal notes" plus "customer comments" on one table isn't possible.
|
|
45
|
+
- Write the object inline. A variable hides it from the build.
|
|
46
|
+
|
|
47
|
+
## Read a thread
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const getThread = createServerFn({ method: 'GET' })
|
|
51
|
+
.validator((data: { recordId: string; offset?: number | null }) => data)
|
|
52
|
+
.handler(({ data }) => comments.list(orderComments, { recordId: data.recordId, offset: data.offset }))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`comments.list` returns `null` when this visitor can't see comments on the record (comments are off, the table isn't set up, or their group can't read). Render nothing for `null`, not an error.
|
|
56
|
+
|
|
57
|
+
Otherwise it returns:
|
|
58
|
+
|
|
59
|
+
| Field | What it is |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `items` | This page's comments, newest first. Each is a `CommentView` |
|
|
62
|
+
| `canPost` | Whether the visitor can post. Hide the composer when false |
|
|
63
|
+
| `allowMentions` | Whether `@` opens a mention menu. Don't call `searchMentions` when false |
|
|
64
|
+
| `allowAttachments` | Whether the composer can attach files |
|
|
65
|
+
| `notificationsEnabled` | Whether to show the notifications switch at all |
|
|
66
|
+
| `quickReactions` | The five `{ emoji, label }` reactions. There are no others |
|
|
67
|
+
| `total`, `nextOffset` | Paging. Pass `nextOffset` back as `offset` to get the next, older page. `null` on the last page |
|
|
68
|
+
|
|
69
|
+
A `CommentView` is `{ id, bodyHtml, attachments, author: { name, avatar, isDeactivated }, createdAt, canDelete, canEdit, edited, source, reactions }`.
|
|
70
|
+
|
|
71
|
+
- `createdAt` is ISO 8601. Show it with `<FormattedDate value={comment.createdAt} time />`, or `useFormatting` for a title; see the `formatting` guidance.
|
|
72
|
+
- `attachments` are `{ url, filename, type }`. `type` is `null` when unknown.
|
|
73
|
+
- `edited` is true once the comment changed after posting.
|
|
74
|
+
- `source` is `{ text, mentions }` for the edit box, or `null` when `canEdit` is false.
|
|
75
|
+
- Each reaction is `{ emoji, label, count, isSelected }`.
|
|
76
|
+
|
|
77
|
+
`bodyHtml` is already sanitised. Render it with `dangerouslySetInnerHTML` and nothing else:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<div className="comment-body" dangerouslySetInnerHTML={{ __html: comment.bodyHtml }} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It holds `p`, `br`, `strong`, `em`, `u`, `s`, `del`, `a`, `ul`, `ol`, `li`, `code`, `pre`, `blockquote`, `h1`–`h3` and mentions (`span[data-type="mention"]`). Links open in a new tab. Style these tags under the body's class: the reset strips bullets, link colour and spacing.
|
|
84
|
+
|
|
85
|
+
## Post a comment
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const postComment = createServerFn({ method: 'POST' })
|
|
89
|
+
.validator(
|
|
90
|
+
(data: {
|
|
91
|
+
recordId: string
|
|
92
|
+
body: string
|
|
93
|
+
mentions: Array<CommentMention>
|
|
94
|
+
attachments?: Array<CommentAttachmentInput>
|
|
95
|
+
}) => data,
|
|
96
|
+
)
|
|
97
|
+
.handler(({ data }) => comments.create(orderComments, data))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Pass the composer's `text` and `mentions` as they are. Mentioned people get an email. A mention whose `@name` was edited posts as plain text.
|
|
101
|
+
|
|
102
|
+
`attachments` are the finished uploads: `useFileUpload(orderComments).files.filter((f) => f.status === 'done')`. `useFileUpload` takes the comments declaration like a mutation. Send files only when `allowAttachments` is true. A comment needs text or at least one file.
|
|
103
|
+
|
|
104
|
+
## The composer
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
const composer = useCommentComposer()
|
|
108
|
+
const { files, isUploading, upload, remove } = useFileUpload(orderComments)
|
|
109
|
+
const done = files.filter((f) => f.status === 'done')
|
|
110
|
+
|
|
111
|
+
<textarea {...composer.textareaProps} placeholder="Write a comment" />
|
|
112
|
+
{/* While composer.query is not null, list findPeople's answer for it. Picking a person calls composer.pick(person). */}
|
|
113
|
+
<button
|
|
114
|
+
disabled={(composer.text.trim() === '' && done.length === 0) || isUploading}
|
|
115
|
+
onClick={async () => {
|
|
116
|
+
await postComment({ data: { recordId, body: composer.text, mentions: composer.mentions, attachments: done } })
|
|
117
|
+
composer.reset()
|
|
118
|
+
done.forEach((f) => remove(f.id))
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
Post
|
|
122
|
+
</button>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- Spread `textareaProps` onto the `<textarea>` and don't override its handlers.
|
|
126
|
+
- `query` is the text after `@` before the caret, or `null`. It's `""` right after `@` is typed.
|
|
127
|
+
- `pick({ name, email })` writes `@name`, records the mention and moves the caret after it.
|
|
128
|
+
- `reset()` empties the box. Call it only after the post succeeds.
|
|
129
|
+
|
|
130
|
+
## Editing a comment
|
|
131
|
+
|
|
132
|
+
Offer Edit only where `canEdit` is true. Mount the edit box only while editing.
|
|
133
|
+
|
|
134
|
+
- Text: `useCommentComposer({ initial: comment.source })`.
|
|
135
|
+
- Files: `useFileUpload(orderComments, { initialAttachments: comment.attachments.map((a) => ({ ...a, id: a.url })) })`. Attachments have no `id`, so the url stands in.
|
|
136
|
+
- Save with `comments.update`, passing the full file list: the kept files plus new uploads. With `allowAttachments` false the server keeps the old files whatever you send.
|
|
137
|
+
|
|
138
|
+
## Mentions
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const findPeople = createServerFn({ method: 'GET' })
|
|
142
|
+
.validator((data: { query: string }) => data)
|
|
143
|
+
.handler(({ data }) => comments.searchMentions(orderComments, data))
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It returns `[{ name, email, avatar }]`: people the visitor can mention whose name or email matches. Who's mentionable is set per user group in Settings → Comments.
|
|
147
|
+
|
|
148
|
+
## Delete, update, react, subscribe
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await comments.delete(orderComments, { recordId, commentId })
|
|
152
|
+
await comments.update(orderComments, { recordId, commentId, body, mentions, attachments })
|
|
153
|
+
await comments.react(orderComments, { recordId, commentId, emoji: '👍', selected: !reaction.isSelected })
|
|
154
|
+
const subscribed = await comments.subscription(orderComments, { recordId })
|
|
155
|
+
await comments.setSubscription(orderComments, { recordId, subscribed: !subscribed })
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Each runs in its own server function, like `list`. Writes return nothing, so re-read the thread after each one.
|
|
159
|
+
|
|
160
|
+
- Offer delete only where `canDelete` is true.
|
|
161
|
+
- `react` adds the reaction when `selected` is true and removes it when false. `emoji` must be one of `quickReactions`.
|
|
162
|
+
- A subscription means the visitor gets an email about new comments on the record. It needs a signed-in visitor. Posting subscribes the author; a mention subscribes the person mentioned.
|
|
163
|
+
- Mails go out a few minutes after a comment, never to its author, and only while notifications are on in Settings → Comments.
|
|
164
|
+
|
|
165
|
+
## Failure modes
|
|
166
|
+
|
|
167
|
+
**Keying the thread on the URL.** Pass the fetched `record.id`, never a route or search param. A param can be a slug or stale, and the thread shows another record's comments.
|
|
168
|
+
|
|
169
|
+
**Treating `null` as an error.** Render nothing. An error box shows every such visitor a broken page.
|
|
170
|
+
|
|
171
|
+
**Rendering a body as text, or sanitising it again.** Use `bodyHtml` with `dangerouslySetInnerHTML`. As text it shows raw tags; a second sanitiser strips the mentions.
|
|
172
|
+
|
|
173
|
+
**Inventing reactions.** Only `quickReactions` can be stored. `react` throws for anything else.
|
|
174
|
+
|
|
175
|
+
**Sending only new files on edit.** `update` replaces the whole list, so kept files you leave out get deleted.
|
|
176
|
+
|
|
177
|
+
**Calling `comments.*` from a component.** It's server only. Wrap each call in a `createServerFn`.
|
|
178
|
+
|
|
179
|
+
## References
|
|
180
|
+
|
|
181
|
+
- [record-queries/SKILL.md](../record-queries/SKILL.md): fetching the record whose `id` keys the thread
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.75
|
|
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.
|
|
@@ -75,9 +75,10 @@ function ApplicationForm() {
|
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
77
|
type FileAttachment = {
|
|
78
|
-
url: string
|
|
79
|
-
filename: string
|
|
80
|
-
id: string
|
|
78
|
+
url: string // clean URL, no internal query params
|
|
79
|
+
filename: string // original filename
|
|
80
|
+
id: string // file id extracted from the upload response
|
|
81
|
+
type?: string | null // content type from the upload response, or null when the platform doesn't know it
|
|
81
82
|
}
|
|
82
83
|
```
|
|
83
84
|
|