@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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/cache.d.ts +109 -0
  4. package/dist/cache.js +94 -0
  5. package/dist/consts.d.ts +28 -0
  6. package/dist/consts.js +26 -0
  7. package/dist/context.d.ts +188 -0
  8. package/dist/context.js +33 -0
  9. package/dist/index.d.ts +1136 -0
  10. package/dist/index.js +24 -0
  11. package/dist/lib/diff.d.ts +39 -0
  12. package/dist/lib/diff.js +29 -0
  13. package/dist/lib/logger.d.ts +31 -0
  14. package/dist/lib/logger.js +131 -0
  15. package/dist/lib/pluginUtils.d.ts +221 -0
  16. package/dist/lib/pluginUtils.js +80 -0
  17. package/dist/modules/auth/index.d.ts +463 -0
  18. package/dist/modules/auth/index.js +412 -0
  19. package/dist/modules/clear/index.d.ts +72 -0
  20. package/dist/modules/clear/index.js +52 -0
  21. package/dist/modules/config/consts.d.ts +32 -0
  22. package/dist/modules/config/consts.js +18 -0
  23. package/dist/modules/config/index.d.ts +100 -0
  24. package/dist/modules/config/index.js +205 -0
  25. package/dist/modules/config/templates/mailer.d.ts +36 -0
  26. package/dist/modules/config/templates/mailer.js +218 -0
  27. package/dist/modules/config/type-utils.d.ts +13 -0
  28. package/dist/modules/config/type-utils.js +11 -0
  29. package/dist/modules/delete/index.d.ts +140 -0
  30. package/dist/modules/delete/index.js +274 -0
  31. package/dist/modules/diffTracking/index.d.ts +188 -0
  32. package/dist/modules/diffTracking/index.js +276 -0
  33. package/dist/modules/get/index.d.ts +272 -0
  34. package/dist/modules/get/index.js +466 -0
  35. package/dist/modules/index.d.ts +1003 -0
  36. package/dist/modules/index.js +37 -0
  37. package/dist/modules/init/index.d.ts +60 -0
  38. package/dist/modules/init/index.js +38 -0
  39. package/dist/modules/middleware/index.d.ts +56 -0
  40. package/dist/modules/middleware/index.js +50 -0
  41. package/dist/modules/notificationSettings/index.d.ts +57 -0
  42. package/dist/modules/notificationSettings/index.js +39 -0
  43. package/dist/modules/plugins/index.d.ts +166 -0
  44. package/dist/modules/plugins/index.js +261 -0
  45. package/dist/modules/post/index.d.ts +305 -0
  46. package/dist/modules/post/index.js +305 -0
  47. package/dist/modules/resetTokenBucket/index.d.ts +91 -0
  48. package/dist/modules/resetTokenBucket/index.js +93 -0
  49. package/dist/modules/rest_api/index.d.ts +92 -0
  50. package/dist/modules/rest_api/index.js +113 -0
  51. package/dist/modules/update/index.d.ts +184 -0
  52. package/dist/modules/update/index.js +174 -0
  53. package/dist/modules/util/collectors.d.ts +261 -0
  54. package/dist/modules/util/collectors.js +141 -0
  55. package/dist/modules/util/folderTree.d.ts +100 -0
  56. package/dist/modules/util/folderTree.js +176 -0
  57. package/dist/modules/util/generators.d.ts +83 -0
  58. package/dist/modules/util/generators.js +106 -0
  59. package/dist/modules/util/getFromNPM.d.ts +191 -0
  60. package/dist/modules/util/getFromNPM.js +100 -0
  61. package/dist/modules/util/index.d.ts +236 -0
  62. package/dist/modules/util/index.js +20 -0
  63. package/dist/modules/util/parsers.d.ts +60 -0
  64. package/dist/modules/util/parsers.js +43 -0
  65. package/dist/modules/util/slugify.d.ts +22 -0
  66. package/dist/modules/util/slugify.js +19 -0
  67. package/dist/modules/util/users.d.ts +99 -0
  68. package/dist/modules/util/users.js +78 -0
  69. package/dist/types.d.ts +360 -0
  70. package/dist/types.js +10 -0
  71. 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
+ };