@withstudiocms/sdk 0.0.0-beta.0 → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/cache.d.ts +109 -0
- package/dist/cache.js +94 -0
- package/dist/consts.d.ts +28 -0
- package/dist/consts.js +26 -0
- package/dist/context.d.ts +188 -0
- package/dist/context.js +33 -0
- package/dist/index.d.ts +1136 -0
- package/dist/index.js +24 -0
- package/dist/lib/diff.d.ts +39 -0
- package/dist/lib/diff.js +29 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.js +131 -0
- package/dist/lib/pluginUtils.d.ts +221 -0
- package/dist/lib/pluginUtils.js +80 -0
- package/dist/modules/auth/index.d.ts +463 -0
- package/dist/modules/auth/index.js +412 -0
- package/dist/modules/clear/index.d.ts +72 -0
- package/dist/modules/clear/index.js +52 -0
- package/dist/modules/config/consts.d.ts +32 -0
- package/dist/modules/config/consts.js +18 -0
- package/dist/modules/config/index.d.ts +100 -0
- package/dist/modules/config/index.js +205 -0
- package/dist/modules/config/templates/mailer.d.ts +36 -0
- package/dist/modules/config/templates/mailer.js +218 -0
- package/dist/modules/config/type-utils.d.ts +13 -0
- package/dist/modules/config/type-utils.js +11 -0
- package/dist/modules/delete/index.d.ts +140 -0
- package/dist/modules/delete/index.js +274 -0
- package/dist/modules/diffTracking/index.d.ts +188 -0
- package/dist/modules/diffTracking/index.js +276 -0
- package/dist/modules/get/index.d.ts +272 -0
- package/dist/modules/get/index.js +466 -0
- package/dist/modules/index.d.ts +1003 -0
- package/dist/modules/index.js +37 -0
- package/dist/modules/init/index.d.ts +60 -0
- package/dist/modules/init/index.js +38 -0
- package/dist/modules/middleware/index.d.ts +56 -0
- package/dist/modules/middleware/index.js +50 -0
- package/dist/modules/notificationSettings/index.d.ts +57 -0
- package/dist/modules/notificationSettings/index.js +39 -0
- package/dist/modules/plugins/index.d.ts +166 -0
- package/dist/modules/plugins/index.js +261 -0
- package/dist/modules/post/index.d.ts +305 -0
- package/dist/modules/post/index.js +305 -0
- package/dist/modules/resetTokenBucket/index.d.ts +91 -0
- package/dist/modules/resetTokenBucket/index.js +93 -0
- package/dist/modules/rest_api/index.d.ts +92 -0
- package/dist/modules/rest_api/index.js +113 -0
- package/dist/modules/update/index.d.ts +184 -0
- package/dist/modules/update/index.js +174 -0
- package/dist/modules/util/collectors.d.ts +261 -0
- package/dist/modules/util/collectors.js +141 -0
- package/dist/modules/util/folderTree.d.ts +100 -0
- package/dist/modules/util/folderTree.js +176 -0
- package/dist/modules/util/generators.d.ts +83 -0
- package/dist/modules/util/generators.js +106 -0
- package/dist/modules/util/getFromNPM.d.ts +191 -0
- package/dist/modules/util/getFromNPM.js +100 -0
- package/dist/modules/util/index.d.ts +236 -0
- package/dist/modules/util/index.js +20 -0
- package/dist/modules/util/parsers.d.ts +60 -0
- package/dist/modules/util/parsers.js +43 -0
- package/dist/modules/util/slugify.d.ts +22 -0
- package/dist/modules/util/slugify.js +19 -0
- package/dist/modules/util/users.d.ts +99 -0
- package/dist/modules/util/users.js +78 -0
- package/dist/types.d.ts +360 -0
- package/dist/types.js +10 -0
- package/package.json +55 -7
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { Effect, type ParseResult } from '@withstudiocms/effect';
|
|
2
|
+
import { type DBCallbackFailure } from '@withstudiocms/kysely';
|
|
3
|
+
import type { DatabaseError } from '@withstudiocms/kysely/core/errors';
|
|
4
|
+
import { DBClientLive } from '../../context.js';
|
|
5
|
+
import type { CombinedPageData, CombinedUserData, FolderNode, MetaOnlyPageData, tsPageDataSelect } from '../../types.js';
|
|
6
|
+
import { type FolderTreeError } from './folderTree.js';
|
|
7
|
+
declare const CollectorError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
8
|
+
readonly _tag: "CollectorError";
|
|
9
|
+
} & Readonly<A>;
|
|
10
|
+
/**
|
|
11
|
+
* Error class for collector errors.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CollectorError extends CollectorError_base<{
|
|
14
|
+
cause: unknown;
|
|
15
|
+
}> {
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Utility function to handle errors in collector functions.
|
|
19
|
+
*
|
|
20
|
+
* @param _try - The function to execute that may throw an error.
|
|
21
|
+
* @returns An effect that either yields the result of the function or a CollectorError.
|
|
22
|
+
*/
|
|
23
|
+
export declare const useCollectorError: <T>(_try: () => T) => Effect.Effect<T, CollectorError, never>;
|
|
24
|
+
/**
|
|
25
|
+
* SDKCollectors
|
|
26
|
+
*
|
|
27
|
+
* Effect generator that wires together database access, folder-tree utilities, and parsing helpers
|
|
28
|
+
* to produce a set of high-level "collector" utilities for assembling richer SDK models from
|
|
29
|
+
* raw database rows.
|
|
30
|
+
*
|
|
31
|
+
* Behavior
|
|
32
|
+
* - Instantiates required dependencies (DB client, folder-tree helpers, parsers) by yielding the
|
|
33
|
+
* corresponding live implementations.
|
|
34
|
+
* - Exposes a small collection of helper effects and functions that perform typed queries,
|
|
35
|
+
* transform results, and compose several queries into complete domain objects.
|
|
36
|
+
*
|
|
37
|
+
* Internal helpers (provided inside the effect)
|
|
38
|
+
* - _getUserData(id: string): Effect that queries StudioCMSUsersTable and decodes the result.
|
|
39
|
+
* - _getPageContent(id: string): Effect that queries StudioCMSPageContent for multi-language page
|
|
40
|
+
* content and decodes the result.
|
|
41
|
+
* - _getOAuthAccountData(id: string): Effect that queries StudioCMSOAuthAccounts for a user's
|
|
42
|
+
* OAuth accounts and decodes the result.
|
|
43
|
+
* - _getUserPermissionsData(id: string): Effect that queries StudioCMSPermissions for a user's
|
|
44
|
+
* permissions and decodes the result.
|
|
45
|
+
*
|
|
46
|
+
* Utility functions
|
|
47
|
+
* - _transformPageDataToMetaOnly:
|
|
48
|
+
* Transforms a CombinedPageData (or array thereof) into its metadata-only representation by
|
|
49
|
+
* stripping large content fields (defaultContent, multiLangContent). Returns an Effect that
|
|
50
|
+
* fails with a CollectorError when transformation fails.
|
|
51
|
+
* - _collectContributorData(ids: readonly string[]):
|
|
52
|
+
* Concurrently fetches user records for a list of contributor IDs and filters out missing
|
|
53
|
+
* results.
|
|
54
|
+
*
|
|
55
|
+
* Main collectors (returned object)
|
|
56
|
+
* - collectCategories(ids: number[]):
|
|
57
|
+
* Uses a codec-backed query to fetch category rows by id.
|
|
58
|
+
* - collectTags(ids: number[]):
|
|
59
|
+
* Uses a codec-backed query to fetch tag rows by id.
|
|
60
|
+
* - collectPageData(page, tree, metaOnly = false):
|
|
61
|
+
* Assembles a complete page model by concurrently collecting:
|
|
62
|
+
* - categories and tags (via parsers -> collectCategories/collectTags),
|
|
63
|
+
* - contributor user data,
|
|
64
|
+
* - author user data,
|
|
65
|
+
* - multi-language page content (when metaOnly is false).
|
|
66
|
+
* It computes a safe slug (special-casing "index" -> "/") and resolves the full URL
|
|
67
|
+
* route by walking the provided folder tree (using findNodesAlongPathToId) when a
|
|
68
|
+
* parentFolder is present.
|
|
69
|
+
* Overloads:
|
|
70
|
+
* - Without metaOnly (or metaOnly === false) returns CombinedPageData.
|
|
71
|
+
* - With metaOnly === true returns MetaOnlyPageData (content stripped via
|
|
72
|
+
* _transformPageDataToMetaOnly).
|
|
73
|
+
* Possible failure modes include DatabaseError, ParseResult.ParseError, FolderTreeError,
|
|
74
|
+
* and CollectorError.
|
|
75
|
+
* - collectUserData(user):
|
|
76
|
+
* Enriches a user row with its OAuth accounts and permissions and returns a CombinedUserData
|
|
77
|
+
* result.
|
|
78
|
+
*
|
|
79
|
+
* Errors
|
|
80
|
+
* - All operations are represented as Effects and may fail with the module's domain errors such
|
|
81
|
+
* as DatabaseError, ParseResult.ParseError, FolderTreeError, and CollectorError.
|
|
82
|
+
*
|
|
83
|
+
* Usage
|
|
84
|
+
* - The effect yields an object with the above collector functions which can be used by other
|
|
85
|
+
* SDK modules to obtain normalized, assembled data for pages, users, tags, and categories.
|
|
86
|
+
*/
|
|
87
|
+
export declare const SDKCollectors: Effect.Effect<{
|
|
88
|
+
collectCategories: (input: {
|
|
89
|
+
readonly [x: number]: number;
|
|
90
|
+
readonly length: number;
|
|
91
|
+
toString: {};
|
|
92
|
+
toLocaleString: {};
|
|
93
|
+
concat: {};
|
|
94
|
+
join: {};
|
|
95
|
+
slice: {};
|
|
96
|
+
indexOf: {};
|
|
97
|
+
lastIndexOf: {};
|
|
98
|
+
every: {};
|
|
99
|
+
some: {};
|
|
100
|
+
forEach: {};
|
|
101
|
+
map: {};
|
|
102
|
+
filter: {};
|
|
103
|
+
reduce: {};
|
|
104
|
+
reduceRight: {};
|
|
105
|
+
find: {};
|
|
106
|
+
findIndex: {};
|
|
107
|
+
entries: {};
|
|
108
|
+
keys: {};
|
|
109
|
+
values: {};
|
|
110
|
+
includes: {};
|
|
111
|
+
flatMap: {};
|
|
112
|
+
flat: {};
|
|
113
|
+
at: {};
|
|
114
|
+
findLast: {};
|
|
115
|
+
findLastIndex: {};
|
|
116
|
+
toReversed: {};
|
|
117
|
+
toSorted: {};
|
|
118
|
+
toSpliced: {};
|
|
119
|
+
with: {};
|
|
120
|
+
[Symbol.iterator]: {};
|
|
121
|
+
readonly [Symbol.unscopables]: {
|
|
122
|
+
readonly [x: number]: boolean | undefined;
|
|
123
|
+
readonly length?: boolean | undefined;
|
|
124
|
+
toString?: boolean | undefined;
|
|
125
|
+
toLocaleString?: boolean | undefined;
|
|
126
|
+
concat?: boolean | undefined;
|
|
127
|
+
join?: boolean | undefined;
|
|
128
|
+
slice?: boolean | undefined;
|
|
129
|
+
indexOf?: boolean | undefined;
|
|
130
|
+
lastIndexOf?: boolean | undefined;
|
|
131
|
+
every?: boolean | undefined;
|
|
132
|
+
some?: boolean | undefined;
|
|
133
|
+
forEach?: boolean | undefined;
|
|
134
|
+
map?: boolean | undefined;
|
|
135
|
+
filter?: boolean | undefined;
|
|
136
|
+
reduce?: boolean | undefined;
|
|
137
|
+
reduceRight?: boolean | undefined;
|
|
138
|
+
find?: boolean | undefined;
|
|
139
|
+
findIndex?: boolean | undefined;
|
|
140
|
+
entries?: boolean | undefined;
|
|
141
|
+
keys?: boolean | undefined;
|
|
142
|
+
values?: boolean | undefined;
|
|
143
|
+
includes?: boolean | undefined;
|
|
144
|
+
flatMap?: boolean | undefined;
|
|
145
|
+
flat?: boolean | undefined;
|
|
146
|
+
at?: boolean | undefined;
|
|
147
|
+
findLast?: boolean | undefined;
|
|
148
|
+
findLastIndex?: boolean | undefined;
|
|
149
|
+
toReversed?: boolean | undefined;
|
|
150
|
+
toSorted?: boolean | undefined;
|
|
151
|
+
toSpliced?: boolean | undefined;
|
|
152
|
+
with?: boolean | undefined;
|
|
153
|
+
[Symbol.iterator]?: boolean | undefined;
|
|
154
|
+
readonly [Symbol.unscopables]?: boolean | undefined;
|
|
155
|
+
};
|
|
156
|
+
}) => Effect.Effect<readonly {
|
|
157
|
+
readonly name: string;
|
|
158
|
+
readonly id: number;
|
|
159
|
+
readonly parent: number | null | undefined;
|
|
160
|
+
readonly description: string;
|
|
161
|
+
readonly slug: string;
|
|
162
|
+
readonly meta: {
|
|
163
|
+
readonly [x: string]: unknown;
|
|
164
|
+
};
|
|
165
|
+
}[], DBCallbackFailure | DatabaseError, never>;
|
|
166
|
+
collectTags: (input: {
|
|
167
|
+
readonly [x: number]: number;
|
|
168
|
+
readonly length: number;
|
|
169
|
+
toString: {};
|
|
170
|
+
toLocaleString: {};
|
|
171
|
+
concat: {};
|
|
172
|
+
join: {};
|
|
173
|
+
slice: {};
|
|
174
|
+
indexOf: {};
|
|
175
|
+
lastIndexOf: {};
|
|
176
|
+
every: {};
|
|
177
|
+
some: {};
|
|
178
|
+
forEach: {};
|
|
179
|
+
map: {};
|
|
180
|
+
filter: {};
|
|
181
|
+
reduce: {};
|
|
182
|
+
reduceRight: {};
|
|
183
|
+
find: {};
|
|
184
|
+
findIndex: {};
|
|
185
|
+
entries: {};
|
|
186
|
+
keys: {};
|
|
187
|
+
values: {};
|
|
188
|
+
includes: {};
|
|
189
|
+
flatMap: {};
|
|
190
|
+
flat: {};
|
|
191
|
+
at: {};
|
|
192
|
+
findLast: {};
|
|
193
|
+
findLastIndex: {};
|
|
194
|
+
toReversed: {};
|
|
195
|
+
toSorted: {};
|
|
196
|
+
toSpliced: {};
|
|
197
|
+
with: {};
|
|
198
|
+
[Symbol.iterator]: {};
|
|
199
|
+
readonly [Symbol.unscopables]: {
|
|
200
|
+
readonly [x: number]: boolean | undefined;
|
|
201
|
+
readonly length?: boolean | undefined;
|
|
202
|
+
toString?: boolean | undefined;
|
|
203
|
+
toLocaleString?: boolean | undefined;
|
|
204
|
+
concat?: boolean | undefined;
|
|
205
|
+
join?: boolean | undefined;
|
|
206
|
+
slice?: boolean | undefined;
|
|
207
|
+
indexOf?: boolean | undefined;
|
|
208
|
+
lastIndexOf?: boolean | undefined;
|
|
209
|
+
every?: boolean | undefined;
|
|
210
|
+
some?: boolean | undefined;
|
|
211
|
+
forEach?: boolean | undefined;
|
|
212
|
+
map?: boolean | undefined;
|
|
213
|
+
filter?: boolean | undefined;
|
|
214
|
+
reduce?: boolean | undefined;
|
|
215
|
+
reduceRight?: boolean | undefined;
|
|
216
|
+
find?: boolean | undefined;
|
|
217
|
+
findIndex?: boolean | undefined;
|
|
218
|
+
entries?: boolean | undefined;
|
|
219
|
+
keys?: boolean | undefined;
|
|
220
|
+
values?: boolean | undefined;
|
|
221
|
+
includes?: boolean | undefined;
|
|
222
|
+
flatMap?: boolean | undefined;
|
|
223
|
+
flat?: boolean | undefined;
|
|
224
|
+
at?: boolean | undefined;
|
|
225
|
+
findLast?: boolean | undefined;
|
|
226
|
+
findLastIndex?: boolean | undefined;
|
|
227
|
+
toReversed?: boolean | undefined;
|
|
228
|
+
toSorted?: boolean | undefined;
|
|
229
|
+
toSpliced?: boolean | undefined;
|
|
230
|
+
with?: boolean | undefined;
|
|
231
|
+
[Symbol.iterator]?: boolean | undefined;
|
|
232
|
+
readonly [Symbol.unscopables]?: boolean | undefined;
|
|
233
|
+
};
|
|
234
|
+
}) => Effect.Effect<readonly {
|
|
235
|
+
readonly name: string;
|
|
236
|
+
readonly id: number;
|
|
237
|
+
readonly description: string;
|
|
238
|
+
readonly slug: string;
|
|
239
|
+
readonly meta: {
|
|
240
|
+
readonly [x: string]: unknown;
|
|
241
|
+
};
|
|
242
|
+
}[], DBCallbackFailure | DatabaseError, never>;
|
|
243
|
+
collectPageData: {
|
|
244
|
+
(page: tsPageDataSelect, tree: FolderNode[]): Effect.Effect<CombinedPageData, CollectorError | FolderTreeError | DBCallbackFailure | DatabaseError | ParseResult.ParseError, never>;
|
|
245
|
+
(page: tsPageDataSelect, tree: FolderNode[], metaOnly: boolean): Effect.Effect<MetaOnlyPageData, CollectorError | FolderTreeError | DBCallbackFailure | DatabaseError | ParseResult.ParseError, never>;
|
|
246
|
+
};
|
|
247
|
+
collectUserData: (user: {
|
|
248
|
+
readonly name: string;
|
|
249
|
+
readonly id: string;
|
|
250
|
+
readonly url: string | null | undefined;
|
|
251
|
+
readonly email: string | null | undefined;
|
|
252
|
+
readonly avatar: string | null | undefined;
|
|
253
|
+
readonly username: string;
|
|
254
|
+
readonly password: string | null | undefined;
|
|
255
|
+
readonly updatedAt: Date;
|
|
256
|
+
readonly createdAt: Date;
|
|
257
|
+
readonly emailVerified: boolean;
|
|
258
|
+
readonly notifications: string | null | undefined;
|
|
259
|
+
}) => Effect.Effect<CombinedUserData, DBCallbackFailure | DatabaseError, never>;
|
|
260
|
+
}, never, DBClientLive>;
|
|
261
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Data, Effect, Schema } from "@withstudiocms/effect";
|
|
2
|
+
import {
|
|
3
|
+
StudioCMSOAuthAccounts,
|
|
4
|
+
StudioCMSPageContent,
|
|
5
|
+
StudioCMSPageDataCategories,
|
|
6
|
+
StudioCMSPageDataTags,
|
|
7
|
+
StudioCMSPermissions,
|
|
8
|
+
StudioCMSUsersTable
|
|
9
|
+
} from "@withstudiocms/kysely";
|
|
10
|
+
import { DBClientLive } from "../../context.js";
|
|
11
|
+
import { SDKFolderTree } from "./folderTree.js";
|
|
12
|
+
import { SDKParsers } from "./parsers.js";
|
|
13
|
+
import { slugify } from "./slugify.js";
|
|
14
|
+
class CollectorError extends Data.TaggedError("CollectorError") {
|
|
15
|
+
}
|
|
16
|
+
const useCollectorError = (_try) => Effect.try({
|
|
17
|
+
try: _try,
|
|
18
|
+
catch: (error) => new CollectorError({ cause: error })
|
|
19
|
+
});
|
|
20
|
+
const SDKCollectors = Effect.gen(function* () {
|
|
21
|
+
const [{ withCodec }, { findNodesAlongPathToId }, { parseIdNumberArray, parseIdStringArray }] = yield* Effect.all([DBClientLive, SDKFolderTree, SDKParsers]);
|
|
22
|
+
const _getUserData = withCodec({
|
|
23
|
+
encoder: Schema.String,
|
|
24
|
+
decoder: Schema.UndefinedOr(StudioCMSUsersTable.Select),
|
|
25
|
+
callbackFn: (query, id) => query(
|
|
26
|
+
(db) => db.selectFrom("StudioCMSUsersTable").selectAll().where("id", "=", id).executeTakeFirst()
|
|
27
|
+
)
|
|
28
|
+
});
|
|
29
|
+
const _getPageContent = withCodec({
|
|
30
|
+
encoder: Schema.String,
|
|
31
|
+
decoder: Schema.Array(StudioCMSPageContent.Select),
|
|
32
|
+
callbackFn: (query, id) => query(
|
|
33
|
+
(db) => db.selectFrom("StudioCMSPageContent").selectAll().where("contentId", "=", id).execute()
|
|
34
|
+
)
|
|
35
|
+
});
|
|
36
|
+
const _getOAuthAccountData = withCodec({
|
|
37
|
+
encoder: Schema.String,
|
|
38
|
+
decoder: Schema.Array(StudioCMSOAuthAccounts.Select),
|
|
39
|
+
callbackFn: (query, id) => query(
|
|
40
|
+
(db) => db.selectFrom("StudioCMSOAuthAccounts").selectAll().where("userId", "=", id).execute()
|
|
41
|
+
)
|
|
42
|
+
});
|
|
43
|
+
const _getUserPermissionsData = withCodec({
|
|
44
|
+
encoder: Schema.String,
|
|
45
|
+
decoder: Schema.UndefinedOr(StudioCMSPermissions.Select),
|
|
46
|
+
callbackFn: (query, id) => query(
|
|
47
|
+
(db) => db.selectFrom("StudioCMSPermissions").selectAll().where("user", "=", id).executeTakeFirst()
|
|
48
|
+
)
|
|
49
|
+
});
|
|
50
|
+
const _transformPageDataToMetaOnly = (data) => useCollectorError(() => {
|
|
51
|
+
if (Array.isArray(data)) {
|
|
52
|
+
return data.map(
|
|
53
|
+
({ defaultContent, multiLangContent, ...rest2 }) => rest2
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const {
|
|
57
|
+
defaultContent: _dump1,
|
|
58
|
+
multiLangContent: _dump2,
|
|
59
|
+
...rest
|
|
60
|
+
} = data;
|
|
61
|
+
return rest;
|
|
62
|
+
});
|
|
63
|
+
const _collectContributorData = Effect.fn(
|
|
64
|
+
(ids) => Effect.all(ids.map((id) => _getUserData(id))).pipe(
|
|
65
|
+
Effect.map((results) => results.filter((user) => !!user))
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
const collectCategories = withCodec({
|
|
69
|
+
encoder: Schema.Array(Schema.Number),
|
|
70
|
+
decoder: Schema.Array(StudioCMSPageDataCategories.Select),
|
|
71
|
+
callbackFn: (db, ids) => db(
|
|
72
|
+
(c) => c.selectFrom("StudioCMSPageDataCategories").selectAll().where("id", "in", ids).execute()
|
|
73
|
+
)
|
|
74
|
+
});
|
|
75
|
+
const collectTags = withCodec({
|
|
76
|
+
encoder: Schema.Array(Schema.Number),
|
|
77
|
+
decoder: Schema.Array(StudioCMSPageDataTags.Select),
|
|
78
|
+
callbackFn: (db, ids) => db((c) => c.selectFrom("StudioCMSPageDataTags").selectAll().where("id", "in", ids).execute())
|
|
79
|
+
});
|
|
80
|
+
function collectPageData(page, tree, metaOnly = false) {
|
|
81
|
+
return Effect.gen(function* () {
|
|
82
|
+
const [categories, tags, contributorsData, authorData] = yield* Effect.all([
|
|
83
|
+
parseIdNumberArray(page.categories || []).pipe(Effect.flatMap(collectCategories)),
|
|
84
|
+
parseIdNumberArray(page.tags || []).pipe(Effect.flatMap(collectTags)),
|
|
85
|
+
parseIdStringArray(page.contributorIds || []).pipe(Effect.flatMap(_collectContributorData)),
|
|
86
|
+
_getUserData(page.authorId)
|
|
87
|
+
]);
|
|
88
|
+
let multiLangContent = [];
|
|
89
|
+
if (!metaOnly) {
|
|
90
|
+
multiLangContent = yield* _getPageContent(page.id);
|
|
91
|
+
}
|
|
92
|
+
const defaultContent = multiLangContent?.find(
|
|
93
|
+
(content) => content.contentLang === page.contentLang
|
|
94
|
+
);
|
|
95
|
+
const safeSlug = page.slug === "index" ? "/" : slugify(page.slug);
|
|
96
|
+
let urlRoute = safeSlug.startsWith("/") ? safeSlug : `/${safeSlug}`;
|
|
97
|
+
if (page.parentFolder) {
|
|
98
|
+
const urlParts = yield* findNodesAlongPathToId(tree, page.parentFolder);
|
|
99
|
+
const folderPath = urlParts.map(({ name }) => slugify(name)).join("/");
|
|
100
|
+
urlRoute = folderPath.length > 0 ? `/${folderPath}${safeSlug === "/" ? "" : `/${safeSlug}`}` : safeSlug;
|
|
101
|
+
}
|
|
102
|
+
const returnData = {
|
|
103
|
+
...page,
|
|
104
|
+
slug: safeSlug,
|
|
105
|
+
urlRoute,
|
|
106
|
+
categories,
|
|
107
|
+
tags,
|
|
108
|
+
authorData,
|
|
109
|
+
contributorsData,
|
|
110
|
+
multiLangContent,
|
|
111
|
+
defaultContent
|
|
112
|
+
};
|
|
113
|
+
if (metaOnly) {
|
|
114
|
+
return yield* _transformPageDataToMetaOnly(returnData);
|
|
115
|
+
}
|
|
116
|
+
return returnData;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const collectUserData = Effect.fn(
|
|
120
|
+
(user) => Effect.all([_getOAuthAccountData(user.id), _getUserPermissionsData(user.id)]).pipe(
|
|
121
|
+
Effect.map(
|
|
122
|
+
([oAuthData, permissionsData]) => ({
|
|
123
|
+
...user,
|
|
124
|
+
oAuthData,
|
|
125
|
+
permissionsData
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
return {
|
|
131
|
+
collectCategories,
|
|
132
|
+
collectTags,
|
|
133
|
+
collectPageData,
|
|
134
|
+
collectUserData
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
export {
|
|
138
|
+
CollectorError,
|
|
139
|
+
SDKCollectors,
|
|
140
|
+
useCollectorError
|
|
141
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Effect } from '@withstudiocms/effect';
|
|
2
|
+
import { DBClientLive } from '../../context.js';
|
|
3
|
+
import type { FolderListItem, FolderNode } from '../../types.js';
|
|
4
|
+
declare const FolderTreeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
5
|
+
readonly _tag: "FolderTreeError";
|
|
6
|
+
} & Readonly<A>;
|
|
7
|
+
/**
|
|
8
|
+
* Custom error class for folder tree related errors.
|
|
9
|
+
*/
|
|
10
|
+
export declare class FolderTreeError extends FolderTreeError_base<{
|
|
11
|
+
cause: unknown;
|
|
12
|
+
}> {
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Utility function to wrap operations with FolderTreeError handling.
|
|
16
|
+
*
|
|
17
|
+
* @param _try - A function that performs the operation to be wrapped.
|
|
18
|
+
* @returns An Effect that either yields the result of the operation or a FolderTreeError.
|
|
19
|
+
*/
|
|
20
|
+
export declare const useFolderTreeError: <T>(_try: () => T) => Effect.Effect<T, FolderTreeError, never>;
|
|
21
|
+
/**
|
|
22
|
+
* SDKFolderTree
|
|
23
|
+
*
|
|
24
|
+
* An Effect.Gen that provides utilities for constructing and querying an in-memory folder tree
|
|
25
|
+
* representation derived from the StudioCMSPageFolderStructure table.
|
|
26
|
+
*
|
|
27
|
+
* This effect:
|
|
28
|
+
* - Fetches the current folder records from the database using DBClientLive and a typed decoder.
|
|
29
|
+
* - Exposes pure and effectful helper functions for building a hierarchical tree, searching nodes
|
|
30
|
+
* by path or id, and adding page nodes to the tree.
|
|
31
|
+
*
|
|
32
|
+
* Exposed functions and behavior:
|
|
33
|
+
* - generateFolderTree(folders: readonly tsPageFolderSelect[]): FolderNode[]
|
|
34
|
+
* Builds a hierarchical FolderNode[] from a flat list of folder records. Each FolderNode
|
|
35
|
+
* contains id, name, pageData, page (boolean), and children (FolderNode[]).
|
|
36
|
+
*
|
|
37
|
+
* - getFullPath(tree: FolderNode[], path: string[]): string[]
|
|
38
|
+
* Returns the sequence of folder names from the root down to the specified path of folder names.
|
|
39
|
+
* If the supplied path is not found, returns the longest matched prefix (or an empty array).
|
|
40
|
+
*
|
|
41
|
+
* - findNodeByPath(tree: FolderNode[], path: string[]): FolderNode | null
|
|
42
|
+
* Locates and returns the FolderNode that exactly matches the supplied path of folder names.
|
|
43
|
+
* Returns null if the full path does not exist.
|
|
44
|
+
*
|
|
45
|
+
* - findNodesAlongPath(tree: FolderNode[], path: string[]): FolderNode[]
|
|
46
|
+
* Returns the array of FolderNode objects encountered along the specified path of names.
|
|
47
|
+
* Useful for obtaining each node in the chain (root → ... → target). Returns an empty array
|
|
48
|
+
* when no match is found.
|
|
49
|
+
*
|
|
50
|
+
* - findNodesAlongPathToId(tree: FolderNode[], id: string): FolderNode[]
|
|
51
|
+
* Searches the tree for a node by id and returns all FolderNode objects along the path from
|
|
52
|
+
* the root to that node. If the id is not found, an empty array is returned.
|
|
53
|
+
*
|
|
54
|
+
* - findNodeById(tree: FolderNode[], id: string): FolderNode | null
|
|
55
|
+
* Recursively searches and returns the FolderNode with the matching id, or null when not found.
|
|
56
|
+
*
|
|
57
|
+
* - addPageToFolderTree(tree: FolderNode[], folderId: string, newPage: FolderNode): FolderNode[]
|
|
58
|
+
* Effectful helper that inserts a new page node under the folder with folderId. If no folder
|
|
59
|
+
* with folderId exists, the newPage is appended at the root level. Returns the updated tree.
|
|
60
|
+
*
|
|
61
|
+
* - buildFolderTree: Effect<unknown, Error, FolderNode[]>
|
|
62
|
+
* Effect that fetches current folders from the DB and maps them to a hierarchical tree using
|
|
63
|
+
* generateFolderTree.
|
|
64
|
+
*
|
|
65
|
+
* - getAvailableFolders: Effect<unknown, Error, FolderListItem[]>
|
|
66
|
+
* Effect that fetches current folders from the DB and returns a light-weight list of folder
|
|
67
|
+
* descriptors { id, name, parent } for UI lists or selection controls.
|
|
68
|
+
*
|
|
69
|
+
* Notes and guarantees:
|
|
70
|
+
* - All helpers wrap their logic in a folder-tree specific error boundary via useFolderTreeError,
|
|
71
|
+
* so callers receive consistent errors for tree operations.
|
|
72
|
+
* - The internal representation assumes unique folder ids and uses the parent id to assemble
|
|
73
|
+
* the hierarchy; folders with a null/undefined parent are treated as root nodes.
|
|
74
|
+
* - The effect depends on DBClientLive and a decoder for StudioCMSPageFolderStructure to ensure
|
|
75
|
+
* type-safe database reads.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* Intended for use by SDK consumers that need to present, navigate, or mutate a folder hierarchy
|
|
79
|
+
* derived from persistent storage. Consumers should treat returned FolderNode[] as a mutable
|
|
80
|
+
* structure when using addPageToFolderTree; other helpers are pure and deterministic.
|
|
81
|
+
*
|
|
82
|
+
* @returns An Effect that yields an object exposing the functions described above for building
|
|
83
|
+
* and querying folder trees.
|
|
84
|
+
*/
|
|
85
|
+
export declare const SDKFolderTree: Effect.Effect<{
|
|
86
|
+
generateFolderTree: (folders: readonly {
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly parent: string | null | undefined;
|
|
90
|
+
}[]) => Effect.Effect<FolderNode[], FolderTreeError, never>;
|
|
91
|
+
getFullPath: (tree: FolderNode[], path: string[]) => Effect.Effect<string[], FolderTreeError, never>;
|
|
92
|
+
findNodeByPath: (tree: FolderNode[], path: string[]) => Effect.Effect<FolderNode | null, FolderTreeError, never>;
|
|
93
|
+
findNodesAlongPath: (tree: FolderNode[], path: string[]) => Effect.Effect<FolderNode[], FolderTreeError, never>;
|
|
94
|
+
findNodesAlongPathToId: (tree: FolderNode[], id: string) => Effect.Effect<FolderNode[], FolderTreeError, never>;
|
|
95
|
+
findNodeById: (tree: FolderNode[], id: string) => Effect.Effect<FolderNode | null, FolderTreeError, never>;
|
|
96
|
+
addPageToFolderTree: (tree: FolderNode[], folderId: string, newPage: FolderNode) => Effect.Effect<FolderNode[], FolderTreeError, never>;
|
|
97
|
+
buildFolderTree: Effect.Effect<FolderNode[], import("@withstudiocms/kysely").DBCallbackFailure | import("@withstudiocms/kysely/core/errors").DatabaseError | FolderTreeError, never>;
|
|
98
|
+
getAvailableFolders: Effect.Effect<FolderListItem[], import("@withstudiocms/kysely").DBCallbackFailure | import("@withstudiocms/kysely/core/errors").DatabaseError, never>;
|
|
99
|
+
}, never, DBClientLive>;
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Data, Effect, Schema } from "@withstudiocms/effect";
|
|
2
|
+
import { StudioCMSPageFolderStructure } from "@withstudiocms/kysely";
|
|
3
|
+
import { DBClientLive } from "../../context.js";
|
|
4
|
+
class FolderTreeError extends Data.TaggedError("FolderTreeError") {
|
|
5
|
+
}
|
|
6
|
+
const useFolderTreeError = (_try) => Effect.try({
|
|
7
|
+
try: _try,
|
|
8
|
+
catch: (error) => new FolderTreeError({ cause: error })
|
|
9
|
+
});
|
|
10
|
+
const SDKFolderTree = Effect.gen(function* () {
|
|
11
|
+
const { withDecoder } = yield* DBClientLive;
|
|
12
|
+
const _getCurrentFolders = withDecoder({
|
|
13
|
+
decoder: Schema.Array(StudioCMSPageFolderStructure.Select),
|
|
14
|
+
callbackFn: (query) => query((db) => db.selectFrom("StudioCMSPageFolderStructure").selectAll().execute())
|
|
15
|
+
});
|
|
16
|
+
const generateFolderTree = Effect.fn(
|
|
17
|
+
(folders) => useFolderTreeError(() => {
|
|
18
|
+
const folderMap = {};
|
|
19
|
+
for (const folder of folders) {
|
|
20
|
+
folderMap[folder.id] = {
|
|
21
|
+
id: folder.id,
|
|
22
|
+
name: folder.name,
|
|
23
|
+
pageData: null,
|
|
24
|
+
page: false,
|
|
25
|
+
children: []
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const rootFolders = [];
|
|
29
|
+
for (const folder of folders) {
|
|
30
|
+
const childFolder = folderMap[folder.id];
|
|
31
|
+
if (!childFolder) continue;
|
|
32
|
+
if (folder.parent === null || folder.parent === void 0) {
|
|
33
|
+
rootFolders.push(childFolder);
|
|
34
|
+
} else {
|
|
35
|
+
const parentFolder = folderMap[folder.parent];
|
|
36
|
+
if (parentFolder) {
|
|
37
|
+
parentFolder.children.push(childFolder);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return rootFolders;
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
const getFullPath = Effect.fn(
|
|
45
|
+
(tree, path) => useFolderTreeError(() => {
|
|
46
|
+
const result = [];
|
|
47
|
+
function helper(nodes, pathParts) {
|
|
48
|
+
if (pathParts.length === 0) return false;
|
|
49
|
+
const [current, ...rest] = pathParts;
|
|
50
|
+
for (const node of nodes) {
|
|
51
|
+
if (node.name === current) {
|
|
52
|
+
result.push(node.name);
|
|
53
|
+
if (rest.length === 0 || helper(node.children, rest)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
result.pop();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
helper(tree, path);
|
|
62
|
+
return result;
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
const findNodeByPath = Effect.fn(
|
|
66
|
+
(tree, path) => useFolderTreeError(() => {
|
|
67
|
+
function _findNodeByPath(tree2, path2) {
|
|
68
|
+
if (path2.length === 0) return null;
|
|
69
|
+
const [current, ...rest] = path2;
|
|
70
|
+
for (const node of tree2) {
|
|
71
|
+
if (node.name === current) {
|
|
72
|
+
if (rest.length === 0) return node;
|
|
73
|
+
return _findNodeByPath(node.children, rest);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return _findNodeByPath(tree, path);
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
const findNodesAlongPath = Effect.fn(
|
|
82
|
+
(tree, path) => useFolderTreeError(() => {
|
|
83
|
+
const result = [];
|
|
84
|
+
function helper(nodes, pathParts) {
|
|
85
|
+
if (pathParts.length === 0) return false;
|
|
86
|
+
const [current, ...rest] = pathParts;
|
|
87
|
+
for (const node of nodes) {
|
|
88
|
+
if (node.name === current) {
|
|
89
|
+
result.push(node);
|
|
90
|
+
if (rest.length === 0 || helper(node.children, rest)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
result.pop();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
helper(tree, path);
|
|
99
|
+
return result;
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
const findNodesAlongPathToId = Effect.fn(
|
|
103
|
+
(tree, id) => useFolderTreeError(() => {
|
|
104
|
+
const path = [];
|
|
105
|
+
function helper(nodes, targetId) {
|
|
106
|
+
for (const node of nodes) {
|
|
107
|
+
path.push(node);
|
|
108
|
+
if (node.id === targetId) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (helper(node.children, targetId)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
path.pop();
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
helper(tree, id);
|
|
119
|
+
return path;
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
const findNodeById = Effect.fn(
|
|
123
|
+
(tree, id) => useFolderTreeError(() => {
|
|
124
|
+
function _findNodeById(tree2, id2) {
|
|
125
|
+
for (const node of tree2) {
|
|
126
|
+
if (node.id === id2) {
|
|
127
|
+
return node;
|
|
128
|
+
}
|
|
129
|
+
const found = _findNodeById(node.children, id2);
|
|
130
|
+
if (found) {
|
|
131
|
+
return found;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return _findNodeById(tree, id);
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
const addPageToFolderTree = Effect.fn(function* (tree, folderId, newPage) {
|
|
140
|
+
const parentFolder = yield* findNodeById(tree, folderId);
|
|
141
|
+
if (!parentFolder) {
|
|
142
|
+
tree.push(newPage);
|
|
143
|
+
return tree;
|
|
144
|
+
}
|
|
145
|
+
parentFolder.children.push(newPage);
|
|
146
|
+
return tree;
|
|
147
|
+
});
|
|
148
|
+
const buildFolderTree = _getCurrentFolders().pipe(Effect.flatMap(generateFolderTree));
|
|
149
|
+
const getAvailableFolders = _getCurrentFolders().pipe(
|
|
150
|
+
Effect.map(
|
|
151
|
+
(folders) => folders.map(
|
|
152
|
+
(folder) => ({
|
|
153
|
+
id: folder.id,
|
|
154
|
+
name: folder.name,
|
|
155
|
+
parent: folder.parent
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
return {
|
|
161
|
+
generateFolderTree,
|
|
162
|
+
getFullPath,
|
|
163
|
+
findNodeByPath,
|
|
164
|
+
findNodesAlongPath,
|
|
165
|
+
findNodesAlongPathToId,
|
|
166
|
+
findNodeById,
|
|
167
|
+
addPageToFolderTree,
|
|
168
|
+
buildFolderTree,
|
|
169
|
+
getAvailableFolders
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
export {
|
|
173
|
+
FolderTreeError,
|
|
174
|
+
SDKFolderTree,
|
|
175
|
+
useFolderTreeError
|
|
176
|
+
};
|