@molecule/api-resource-bookmark 1.0.0

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 (44) hide show
  1. package/LICENSE +115 -0
  2. package/dist/authorizers/index.d.ts +10 -0
  3. package/dist/authorizers/index.d.ts.map +1 -0
  4. package/dist/authorizers/index.js +9 -0
  5. package/dist/browser-guard.d.ts +2 -0
  6. package/dist/browser-guard.d.ts.map +1 -0
  7. package/dist/browser-guard.js +18 -0
  8. package/dist/handlers/create.d.ts +14 -0
  9. package/dist/handlers/create.d.ts.map +1 -0
  10. package/dist/handlers/create.js +44 -0
  11. package/dist/handlers/del.d.ts +14 -0
  12. package/dist/handlers/del.d.ts.map +1 -0
  13. package/dist/handlers/del.js +47 -0
  14. package/dist/handlers/index.d.ts +6 -0
  15. package/dist/handlers/index.d.ts.map +1 -0
  16. package/dist/handlers/index.js +5 -0
  17. package/dist/handlers/list.d.ts +14 -0
  18. package/dist/handlers/list.d.ts.map +1 -0
  19. package/dist/handlers/list.js +41 -0
  20. package/dist/handlers/read.d.ts +14 -0
  21. package/dist/handlers/read.d.ts.map +1 -0
  22. package/dist/handlers/read.js +47 -0
  23. package/dist/handlers/update.d.ts +14 -0
  24. package/dist/handlers/update.d.ts.map +1 -0
  25. package/dist/handlers/update.js +37 -0
  26. package/dist/index.d.ts +83 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +82 -0
  29. package/dist/requestHandlerMap.d.ts +21 -0
  30. package/dist/requestHandlerMap.d.ts.map +1 -0
  31. package/dist/requestHandlerMap.js +20 -0
  32. package/dist/routes.d.ts +37 -0
  33. package/dist/routes.d.ts.map +1 -0
  34. package/dist/routes.js +27 -0
  35. package/dist/service.d.ts +49 -0
  36. package/dist/service.d.ts.map +1 -0
  37. package/dist/service.js +108 -0
  38. package/dist/types.d.ts +54 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +9 -0
  41. package/dist/validation.d.ts +15 -0
  42. package/dist/validation.d.ts.map +1 -0
  43. package/dist/validation.js +17 -0
  44. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,115 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship.
43
+
44
+ "Contribution" shall mean any work of authorship, including the
45
+ original version of the Work and any modifications or additions
46
+ to that Work, that is intentionally submitted to the Licensor for
47
+ inclusion in the Work by the copyright owner or by an individual or
48
+ Legal Entity authorized to submit on behalf of the copyright owner.
49
+
50
+ "Contributor" shall mean Licensor and any individual or Legal Entity
51
+ on behalf of whom a Contribution has been received by the Licensor and
52
+ subsequently incorporated within the Work.
53
+
54
+ 2. Grant of Copyright License. Subject to the terms and conditions of
55
+ this License, each Contributor hereby grants to You a perpetual,
56
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
57
+ copyright license to reproduce, prepare Derivative Works of,
58
+ publicly display, publicly perform, sublicense, and distribute the
59
+ Work and such Derivative Works in Source or Object form.
60
+
61
+ 3. Grant of Patent License. Subject to the terms and conditions of
62
+ this License, each Contributor hereby grants to You a perpetual,
63
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
64
+ patent license to make, have made, use, offer to sell, sell, import,
65
+ and otherwise transfer the Work.
66
+
67
+ 4. Redistribution. You may reproduce and distribute copies of the
68
+ Work or Derivative Works thereof in any medium, with or without
69
+ modifications, and in Source or Object form, provided that You
70
+ meet the following conditions:
71
+
72
+ (a) You must give any other recipients of the Work or
73
+ Derivative Works a copy of this License; and
74
+
75
+ (b) You must cause any modified files to carry prominent notices
76
+ stating that You changed the files; and
77
+
78
+ (c) You must retain, in the Source form of any Derivative Works
79
+ that You distribute, all copyright, patent, trademark, and
80
+ attribution notices from the Source form of the Work,
81
+ excluding those notices that do not pertain to any part of
82
+ the Derivative Works; and
83
+
84
+ (d) If the Work includes a "NOTICE" text file as part of its
85
+ distribution, then any Derivative Works that You distribute must
86
+ include a readable copy of the attribution notices contained
87
+ within such NOTICE file.
88
+
89
+ 5. Submission of Contributions.
90
+
91
+ 6. Trademarks. This License does not grant permission to use the trade
92
+ names, trademarks, service marks, or product names of the Licensor.
93
+
94
+ 7. Disclaimer of Warranty. Unless required by applicable law or
95
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
96
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
97
+
98
+ 8. Limitation of Liability. In no event and under no legal theory shall
99
+ any Contributor be liable to You for damages.
100
+
101
+ 9. Accepting Warranty or Additional Liability.
102
+
103
+ Copyright 2026 Molecule Dev, Inc.
104
+
105
+ Licensed under the Apache License, Version 2.0 (the "License");
106
+ you may not use this file except in compliance with the License.
107
+ You may obtain a copy of the License at
108
+
109
+ http://www.apache.org/licenses/LICENSE-2.0
110
+
111
+ Unless required by applicable law or agreed to in writing, software
112
+ distributed under the License is distributed on an "AS IS" BASIS,
113
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
114
+ See the License for the specific language governing permissions and
115
+ limitations under the License.
@@ -0,0 +1,10 @@
1
+ export {};
2
+ /**
3
+ * Bookmark authorizers.
4
+ *
5
+ * All bookmark operations require authentication and are scoped to the
6
+ * current user via session. No additional authorizers needed.
7
+ *
8
+ * @module
9
+ */
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/authorizers/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG"}
@@ -0,0 +1,9 @@
1
+ export {};
2
+ /**
3
+ * Bookmark authorizers.
4
+ *
5
+ * All bookmark operations require authentication and are scoped to the
6
+ * current user via session. No additional authorizers needed.
7
+ *
8
+ * @module
9
+ */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser guard — `@molecule/api-resource-bookmark` is SERVER-ONLY.
3
+ *
4
+ * Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
5
+ * Evaluating a server package in a browser bundle is always an import-graph mistake
6
+ * (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
7
+ * ("Buffer is not defined") far from the culprit. Throwing here names the package and
8
+ * the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
9
+ * unaffected: the throw requires browser globals AND the absence of a node runtime.
10
+ */
11
+ const g = globalThis;
12
+ if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
13
+ throw new Error('@molecule/api-resource-bookmark is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
14
+ 'from server code (a server route/function or your API), or dynamic-import it inside ' +
15
+ 'the server handler — never from components or shared client modules, and never ' +
16
+ 'polyfill Buffer/process to silence this.');
17
+ }
18
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Add bookmark handler.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Adds a bookmark for the current user. Idempotent.
9
+ *
10
+ * @param req - The request with bookmark body (resourceType, resourceId, folder?).
11
+ * @param res - The response object.
12
+ */
13
+ export declare function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
14
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/handlers/create.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAK/E;;;;;GAKG;AACH,wBAAsB,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCvF"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Add bookmark handler.
3
+ *
4
+ * @module
5
+ */
6
+ import { t } from '@molecule/api-i18n';
7
+ import { logger } from '@molecule/api-logger';
8
+ import { addBookmark } from '../service.js';
9
+ import { createBookmarkSchema } from '../validation.js';
10
+ /**
11
+ * Adds a bookmark for the current user. Idempotent.
12
+ *
13
+ * @param req - The request with bookmark body (resourceType, resourceId, folder?).
14
+ * @param res - The response object.
15
+ */
16
+ export async function create(req, res) {
17
+ const userId = res.locals.session?.userId;
18
+ if (!userId) {
19
+ res.status(401).json({
20
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
21
+ errorKey: 'resource.error.unauthorized',
22
+ });
23
+ return;
24
+ }
25
+ const parsed = createBookmarkSchema.safeParse(req.body);
26
+ if (!parsed.success) {
27
+ const errors = parsed.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
28
+ res.status(400).json({ error: errors, errorKey: 'bookmark.error.validationFailed' });
29
+ return;
30
+ }
31
+ try {
32
+ const bookmark = await addBookmark(userId, parsed.data.resourceType, parsed.data.resourceId, parsed.data.folder);
33
+ res.status(201).json(bookmark);
34
+ }
35
+ catch (error) {
36
+ logger.error('Failed to add bookmark', { userId, error });
37
+ res.status(500).json({
38
+ error: t('bookmark.error.createFailed', undefined, {
39
+ defaultValue: 'Failed to add bookmark',
40
+ }),
41
+ errorKey: 'bookmark.error.createFailed',
42
+ });
43
+ }
44
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Remove bookmark handler.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Removes a bookmark by resource type and ID.
9
+ *
10
+ * @param req - The request with `resourceType` and `resourceId` params.
11
+ * @param res - The response object.
12
+ */
13
+ export declare function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
14
+ //# sourceMappingURL=del.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"del.d.ts","sourceRoot":"","sources":["../../src/handlers/del.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI/E;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCpF"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Remove bookmark handler.
3
+ *
4
+ * @module
5
+ */
6
+ import { t } from '@molecule/api-i18n';
7
+ import { logger } from '@molecule/api-logger';
8
+ import { removeBookmark } from '../service.js';
9
+ /**
10
+ * Removes a bookmark by resource type and ID.
11
+ *
12
+ * @param req - The request with `resourceType` and `resourceId` params.
13
+ * @param res - The response object.
14
+ */
15
+ export async function del(req, res) {
16
+ const userId = res.locals.session?.userId;
17
+ if (!userId) {
18
+ res.status(401).json({
19
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
20
+ errorKey: 'resource.error.unauthorized',
21
+ });
22
+ return;
23
+ }
24
+ const { resourceType, resourceId } = req.params;
25
+ if (!resourceType || !resourceId) {
26
+ res.status(400).json({
27
+ error: t('bookmark.error.missingResource', undefined, {
28
+ defaultValue: 'Resource type and ID are required',
29
+ }),
30
+ errorKey: 'bookmark.error.missingResource',
31
+ });
32
+ return;
33
+ }
34
+ try {
35
+ await removeBookmark(userId, resourceType, resourceId);
36
+ res.status(204).end();
37
+ }
38
+ catch (error) {
39
+ logger.error('Failed to remove bookmark', { userId, resourceType, resourceId, error });
40
+ res.status(500).json({
41
+ error: t('bookmark.error.deleteFailed', undefined, {
42
+ defaultValue: 'Failed to remove bookmark',
43
+ }),
44
+ errorKey: 'bookmark.error.deleteFailed',
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,6 @@
1
+ export * from './create.js';
2
+ export * from './del.js';
3
+ export * from './list.js';
4
+ export * from './read.js';
5
+ export * from './update.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA"}
@@ -0,0 +1,5 @@
1
+ export * from './create.js';
2
+ export * from './del.js';
3
+ export * from './list.js';
4
+ export * from './read.js';
5
+ export * from './update.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * List bookmarks handler.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Lists the current user's bookmarks with optional filtering and pagination.
9
+ *
10
+ * @param req - The request with optional query params (resourceType, folder, limit, offset).
11
+ * @param res - The response object.
12
+ */
13
+ export declare function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
14
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/handlers/list.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI/E;;;;;GAKG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BrF"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * List bookmarks handler.
3
+ *
4
+ * @module
5
+ */
6
+ import { t } from '@molecule/api-i18n';
7
+ import { logger } from '@molecule/api-logger';
8
+ import { getBookmarks } from '../service.js';
9
+ /**
10
+ * Lists the current user's bookmarks with optional filtering and pagination.
11
+ *
12
+ * @param req - The request with optional query params (resourceType, folder, limit, offset).
13
+ * @param res - The response object.
14
+ */
15
+ export async function list(req, res) {
16
+ const userId = res.locals.session?.userId;
17
+ if (!userId) {
18
+ res.status(401).json({
19
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
20
+ errorKey: 'resource.error.unauthorized',
21
+ });
22
+ return;
23
+ }
24
+ const resourceType = req.query.resourceType;
25
+ const folder = req.query.folder;
26
+ const limit = parseInt(req.query.limit, 10) || 20;
27
+ const offset = parseInt(req.query.offset, 10) || 0;
28
+ try {
29
+ const result = await getBookmarks(userId, { resourceType, folder, limit, offset });
30
+ res.json(result);
31
+ }
32
+ catch (error) {
33
+ logger.error('Failed to list bookmarks', { userId, error });
34
+ res.status(500).json({
35
+ error: t('bookmark.error.listFailed', undefined, {
36
+ defaultValue: 'Failed to list bookmarks',
37
+ }),
38
+ errorKey: 'bookmark.error.listFailed',
39
+ });
40
+ }
41
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Check bookmark handler.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Checks whether the current user has bookmarked a resource.
9
+ *
10
+ * @param req - The request with `resourceType` and `resourceId` params.
11
+ * @param res - The response object.
12
+ */
13
+ export declare function check(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
14
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/handlers/read.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI/E;;;;;GAKG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCtF"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Check bookmark handler.
3
+ *
4
+ * @module
5
+ */
6
+ import { t } from '@molecule/api-i18n';
7
+ import { logger } from '@molecule/api-logger';
8
+ import { isBookmarked } from '../service.js';
9
+ /**
10
+ * Checks whether the current user has bookmarked a resource.
11
+ *
12
+ * @param req - The request with `resourceType` and `resourceId` params.
13
+ * @param res - The response object.
14
+ */
15
+ export async function check(req, res) {
16
+ const userId = res.locals.session?.userId;
17
+ if (!userId) {
18
+ res.status(401).json({
19
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
20
+ errorKey: 'resource.error.unauthorized',
21
+ });
22
+ return;
23
+ }
24
+ const { resourceType, resourceId } = req.params;
25
+ if (!resourceType || !resourceId) {
26
+ res.status(400).json({
27
+ error: t('bookmark.error.missingResource', undefined, {
28
+ defaultValue: 'Resource type and ID are required',
29
+ }),
30
+ errorKey: 'bookmark.error.missingResource',
31
+ });
32
+ return;
33
+ }
34
+ try {
35
+ const bookmarked = await isBookmarked(userId, resourceType, resourceId);
36
+ res.json({ bookmarked });
37
+ }
38
+ catch (error) {
39
+ logger.error('Failed to check bookmark', { userId, resourceType, resourceId, error });
40
+ res.status(500).json({
41
+ error: t('bookmark.error.checkFailed', undefined, {
42
+ defaultValue: 'Failed to check bookmark',
43
+ }),
44
+ errorKey: 'bookmark.error.checkFailed',
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * List bookmark folders handler.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Lists all unique folder names for the current user's bookmarks.
9
+ *
10
+ * @param _req - The request (unused).
11
+ * @param res - The response object.
12
+ */
13
+ export declare function folders(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
14
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/handlers/update.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI/E;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBzF"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * List bookmark folders handler.
3
+ *
4
+ * @module
5
+ */
6
+ import { t } from '@molecule/api-i18n';
7
+ import { logger } from '@molecule/api-logger';
8
+ import { getFolders } from '../service.js';
9
+ /**
10
+ * Lists all unique folder names for the current user's bookmarks.
11
+ *
12
+ * @param _req - The request (unused).
13
+ * @param res - The response object.
14
+ */
15
+ export async function folders(_req, res) {
16
+ const userId = res.locals.session?.userId;
17
+ if (!userId) {
18
+ res.status(401).json({
19
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
20
+ errorKey: 'resource.error.unauthorized',
21
+ });
22
+ return;
23
+ }
24
+ try {
25
+ const result = await getFolders(userId);
26
+ res.json({ folders: result });
27
+ }
28
+ catch (error) {
29
+ logger.error('Failed to list bookmark folders', { userId, error });
30
+ res.status(500).json({
31
+ error: t('bookmark.error.foldersFailed', undefined, {
32
+ defaultValue: 'Failed to list folders',
33
+ }),
34
+ errorKey: 'bookmark.error.foldersFailed',
35
+ });
36
+ }
37
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Bookmark/favorite resource for molecule.dev.
3
+ *
4
+ * Allows users to bookmark any resource, organize into folders, and check
5
+ * bookmark status.
6
+ *
7
+ * @module
8
+ * @example
9
+ * ```typescript
10
+ * import { routes, requestHandlerMap } from '@molecule/api-resource-bookmark'
11
+ *
12
+ * // Wire routes into your Express app via mlcl inject
13
+ * // POST /bookmarks
14
+ * // GET /bookmarks
15
+ * // GET /bookmarks/folders
16
+ * // GET /bookmarks/check/:resourceType/:resourceId
17
+ * // DELETE /bookmarks/:resourceType/:resourceId
18
+ * ```
19
+ *
20
+ * @remarks
21
+ * - **List endpoints return a PAGINATED envelope** `{ data, total, limit, offset }`, not a
22
+ * bare array — read the rows off `result.data` (server). On the client, `unwrapList(res)`
23
+ * from `@molecule/app-http` normalizes this envelope (pass it the whole HttpResponse), so
24
+ * the rows come back; reading the response as a bare array — or `res.data` alone (which is
25
+ * the envelope) — yields an EMPTY list.
26
+ * - **Migration required.** `src/__setup__/bookmarks.sql` ships with this package
27
+ * and must exist in the target database before use (scaffolded apps apply it
28
+ * automatically). Note the `UNIQUE ("userId","resourceType","resourceId")`
29
+ * constraint — one bookmark per user per resource.
30
+ * - **`addBookmark()` is idempotent and does NOT move folders.** Re-adding an
31
+ * existing bookmark returns the existing row unchanged — to move a bookmark to
32
+ * another folder, remove and re-add it (or add your own update path).
33
+ * - **Owner-scoped via the session.** All routes require `authenticate` and every
34
+ * query filters by the session `userId` — never accept a target userId from the
35
+ * client (IDOR).
36
+ * - Bookmarked resources are polymorphic and unverified (no FK): use the same
37
+ * canonical `resourceType` slugs as your other polymorphic resources
38
+ * (comments, activity feed) so `check`/`remove` keys line up.
39
+ * - Folders are free-form strings on the bookmark row (`GET /bookmarks/folders`
40
+ * returns the distinct set) — there is no folder entity to create first.
41
+ *
42
+ * @e2e
43
+ * Integration checklist — drive the real UI (live preview, no mocks), adapt
44
+ * each item to this app's actual screens/flows, and check every box off one
45
+ * by one. A box you can't check is an integration bug to fix — not a skip:
46
+ * - [ ] Bookmarking an item (POST /bookmarks with its resourceType +
47
+ * resourceId) saves it, it then appears in the user's bookmarks list
48
+ * (GET /bookmarks), and its bookmark/star control reflects the saved state
49
+ * (GET /bookmarks/check/:resourceType/:resourceId returns { bookmarked: true }).
50
+ * - [ ] It is idempotent and toggles cleanly: bookmarking the SAME
51
+ * resourceType+resourceId twice does NOT create a duplicate (the UNIQUE
52
+ * (userId, resourceType, resourceId) constraint holds — the second add
53
+ * returns the existing row), and un-bookmarking
54
+ * (DELETE /bookmarks/:resourceType/:resourceId) removes it from the list and
55
+ * flips the control back (check returns { bookmarked: false }).
56
+ * - [ ] Folders work: a bookmark filed into a folder (the free-form `folder`
57
+ * string, set at create time — re-adding does NOT move it between folders)
58
+ * shows under that folder, GET /bookmarks/folders returns the distinct folder
59
+ * set, and GET /bookmarks?folder=X (and ?resourceType=X) filters the list to
60
+ * only the matching bookmarks.
61
+ * - [ ] Display data resolves from the referenced resource: the bookmark row
62
+ * stores only resourceType+resourceId (no title/url/thumbnail, no FK), so each
63
+ * list item renders its real title/thumbnail by looking the target up, and a
64
+ * bookmark whose target was since deleted is handled gracefully (hidden or
65
+ * tombstoned, never a crash or a blank row).
66
+ * - [ ] Authorization — bookmarks are strictly per-user: the owner is the
67
+ * session userId (res.locals.session), NEVER a userId taken from the request
68
+ * body; every list/check/remove is scoped to that session user, so one user
69
+ * can neither see nor delete another user's saved items (there is no
70
+ * bookmark-id route to guess — keys are resourceType+resourceId under the
71
+ * caller's own userId); and a user can only bookmark targets they are allowed
72
+ * to see (the target is polymorphic and unverified, so gate the create by
73
+ * target visibility).
74
+ */
75
+ export * from './authorizers/index.js';
76
+ export * from './browser-guard.js';
77
+ export * from './handlers/index.js';
78
+ export * from './requestHandlerMap.js';
79
+ export * from './routes.js';
80
+ export * from './service.js';
81
+ export * from './types.js';
82
+ export * from './validation.js';
83
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AAEH,cAAc,wBAAwB,CAAA;AACtC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,wBAAwB,CAAA;AACtC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Bookmark/favorite resource for molecule.dev.
3
+ *
4
+ * Allows users to bookmark any resource, organize into folders, and check
5
+ * bookmark status.
6
+ *
7
+ * @module
8
+ * @example
9
+ * ```typescript
10
+ * import { routes, requestHandlerMap } from '@molecule/api-resource-bookmark'
11
+ *
12
+ * // Wire routes into your Express app via mlcl inject
13
+ * // POST /bookmarks
14
+ * // GET /bookmarks
15
+ * // GET /bookmarks/folders
16
+ * // GET /bookmarks/check/:resourceType/:resourceId
17
+ * // DELETE /bookmarks/:resourceType/:resourceId
18
+ * ```
19
+ *
20
+ * @remarks
21
+ * - **List endpoints return a PAGINATED envelope** `{ data, total, limit, offset }`, not a
22
+ * bare array — read the rows off `result.data` (server). On the client, `unwrapList(res)`
23
+ * from `@molecule/app-http` normalizes this envelope (pass it the whole HttpResponse), so
24
+ * the rows come back; reading the response as a bare array — or `res.data` alone (which is
25
+ * the envelope) — yields an EMPTY list.
26
+ * - **Migration required.** `src/__setup__/bookmarks.sql` ships with this package
27
+ * and must exist in the target database before use (scaffolded apps apply it
28
+ * automatically). Note the `UNIQUE ("userId","resourceType","resourceId")`
29
+ * constraint — one bookmark per user per resource.
30
+ * - **`addBookmark()` is idempotent and does NOT move folders.** Re-adding an
31
+ * existing bookmark returns the existing row unchanged — to move a bookmark to
32
+ * another folder, remove and re-add it (or add your own update path).
33
+ * - **Owner-scoped via the session.** All routes require `authenticate` and every
34
+ * query filters by the session `userId` — never accept a target userId from the
35
+ * client (IDOR).
36
+ * - Bookmarked resources are polymorphic and unverified (no FK): use the same
37
+ * canonical `resourceType` slugs as your other polymorphic resources
38
+ * (comments, activity feed) so `check`/`remove` keys line up.
39
+ * - Folders are free-form strings on the bookmark row (`GET /bookmarks/folders`
40
+ * returns the distinct set) — there is no folder entity to create first.
41
+ *
42
+ * @e2e
43
+ * Integration checklist — drive the real UI (live preview, no mocks), adapt
44
+ * each item to this app's actual screens/flows, and check every box off one
45
+ * by one. A box you can't check is an integration bug to fix — not a skip:
46
+ * - [ ] Bookmarking an item (POST /bookmarks with its resourceType +
47
+ * resourceId) saves it, it then appears in the user's bookmarks list
48
+ * (GET /bookmarks), and its bookmark/star control reflects the saved state
49
+ * (GET /bookmarks/check/:resourceType/:resourceId returns { bookmarked: true }).
50
+ * - [ ] It is idempotent and toggles cleanly: bookmarking the SAME
51
+ * resourceType+resourceId twice does NOT create a duplicate (the UNIQUE
52
+ * (userId, resourceType, resourceId) constraint holds — the second add
53
+ * returns the existing row), and un-bookmarking
54
+ * (DELETE /bookmarks/:resourceType/:resourceId) removes it from the list and
55
+ * flips the control back (check returns { bookmarked: false }).
56
+ * - [ ] Folders work: a bookmark filed into a folder (the free-form `folder`
57
+ * string, set at create time — re-adding does NOT move it between folders)
58
+ * shows under that folder, GET /bookmarks/folders returns the distinct folder
59
+ * set, and GET /bookmarks?folder=X (and ?resourceType=X) filters the list to
60
+ * only the matching bookmarks.
61
+ * - [ ] Display data resolves from the referenced resource: the bookmark row
62
+ * stores only resourceType+resourceId (no title/url/thumbnail, no FK), so each
63
+ * list item renders its real title/thumbnail by looking the target up, and a
64
+ * bookmark whose target was since deleted is handled gracefully (hidden or
65
+ * tombstoned, never a crash or a blank row).
66
+ * - [ ] Authorization — bookmarks are strictly per-user: the owner is the
67
+ * session userId (res.locals.session), NEVER a userId taken from the request
68
+ * body; every list/check/remove is scoped to that session user, so one user
69
+ * can neither see nor delete another user's saved items (there is no
70
+ * bookmark-id route to guess — keys are resourceType+resourceId under the
71
+ * caller's own userId); and a user can only bookmark targets they are allowed
72
+ * to see (the target is polymorphic and unverified, so gate the create by
73
+ * target visibility).
74
+ */
75
+ export * from './authorizers/index.js';
76
+ export * from './browser-guard.js';
77
+ export * from './handlers/index.js';
78
+ export * from './requestHandlerMap.js';
79
+ export * from './routes.js';
80
+ export * from './service.js';
81
+ export * from './types.js';
82
+ export * from './validation.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Maps route handler names to their implementations.
3
+ *
4
+ * @module
5
+ */
6
+ import { create } from './handlers/create.js';
7
+ import { del } from './handlers/del.js';
8
+ import { list } from './handlers/list.js';
9
+ import { check } from './handlers/read.js';
10
+ import { folders } from './handlers/update.js';
11
+ /**
12
+ * Handler map for bookmark routes.
13
+ */
14
+ export declare const requestHandlerMap: {
15
+ readonly create: typeof create;
16
+ readonly list: typeof list;
17
+ readonly check: typeof check;
18
+ readonly folders: typeof folders;
19
+ readonly del: typeof del;
20
+ };
21
+ //# sourceMappingURL=requestHandlerMap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestHandlerMap.d.ts","sourceRoot":"","sources":["../src/requestHandlerMap.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAE9C;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;CAMpB,CAAA"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Maps route handler names to their implementations.
3
+ *
4
+ * @module
5
+ */
6
+ import { create } from './handlers/create.js';
7
+ import { del } from './handlers/del.js';
8
+ import { list } from './handlers/list.js';
9
+ import { check } from './handlers/read.js';
10
+ import { folders } from './handlers/update.js';
11
+ /**
12
+ * Handler map for bookmark routes.
13
+ */
14
+ export const requestHandlerMap = {
15
+ create,
16
+ list,
17
+ check,
18
+ folders,
19
+ del,
20
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Bookmark route definitions.
3
+ *
4
+ * Declarative routing — mlcl inject reads these to generate the Express router.
5
+ *
6
+ * @module
7
+ */
8
+ /**
9
+ * Routes for bookmark add/remove/list/check and folder listing.
10
+ */
11
+ export declare const routes: readonly [{
12
+ readonly method: "post";
13
+ readonly path: "/bookmarks";
14
+ readonly handler: "create";
15
+ readonly middlewares: readonly ["authenticate"];
16
+ }, {
17
+ readonly method: "get";
18
+ readonly path: "/bookmarks";
19
+ readonly handler: "list";
20
+ readonly middlewares: readonly ["authenticate"];
21
+ }, {
22
+ readonly method: "get";
23
+ readonly path: "/bookmarks/folders";
24
+ readonly handler: "folders";
25
+ readonly middlewares: readonly ["authenticate"];
26
+ }, {
27
+ readonly method: "get";
28
+ readonly path: "/bookmarks/check/:resourceType/:resourceId";
29
+ readonly handler: "check";
30
+ readonly middlewares: readonly ["authenticate"];
31
+ }, {
32
+ readonly method: "delete";
33
+ readonly path: "/bookmarks/:resourceType/:resourceId";
34
+ readonly handler: "del";
35
+ readonly middlewares: readonly ["authenticate"];
36
+ }];
37
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;EAgBT,CAAA"}
package/dist/routes.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Bookmark route definitions.
3
+ *
4
+ * Declarative routing — mlcl inject reads these to generate the Express router.
5
+ *
6
+ * @module
7
+ */
8
+ /**
9
+ * Routes for bookmark add/remove/list/check and folder listing.
10
+ */
11
+ export const routes = [
12
+ { method: 'post', path: '/bookmarks', handler: 'create', middlewares: ['authenticate'] },
13
+ { method: 'get', path: '/bookmarks', handler: 'list', middlewares: ['authenticate'] },
14
+ { method: 'get', path: '/bookmarks/folders', handler: 'folders', middlewares: ['authenticate'] },
15
+ {
16
+ method: 'get',
17
+ path: '/bookmarks/check/:resourceType/:resourceId',
18
+ handler: 'check',
19
+ middlewares: ['authenticate'],
20
+ },
21
+ {
22
+ method: 'delete',
23
+ path: '/bookmarks/:resourceType/:resourceId',
24
+ handler: 'del',
25
+ middlewares: ['authenticate'],
26
+ },
27
+ ];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Bookmark business logic service.
3
+ *
4
+ * @module
5
+ */
6
+ import type { Bookmark, BookmarkQuery, PaginatedResult } from './types.js';
7
+ /**
8
+ * Adds a bookmark. Idempotent — returns existing bookmark if already bookmarked.
9
+ *
10
+ * @param userId - The user ID.
11
+ * @param resourceType - The type of resource to bookmark.
12
+ * @param resourceId - The ID of the resource to bookmark.
13
+ * @param folder - Optional folder name.
14
+ * @returns The created or existing bookmark.
15
+ */
16
+ export declare function addBookmark(userId: string, resourceType: string, resourceId: string, folder?: string): Promise<Bookmark>;
17
+ /**
18
+ * Removes a bookmark.
19
+ *
20
+ * @param userId - The user ID.
21
+ * @param resourceType - The type of resource.
22
+ * @param resourceId - The ID of the resource.
23
+ */
24
+ export declare function removeBookmark(userId: string, resourceType: string, resourceId: string): Promise<void>;
25
+ /**
26
+ * Gets all bookmarks for a user with optional filtering and pagination.
27
+ *
28
+ * @param userId - The user ID.
29
+ * @param options - Query options.
30
+ * @returns Paginated bookmarks.
31
+ */
32
+ export declare function getBookmarks(userId: string, options?: BookmarkQuery): Promise<PaginatedResult<Bookmark>>;
33
+ /**
34
+ * Checks if a resource is bookmarked by a user.
35
+ *
36
+ * @param userId - The user ID.
37
+ * @param resourceType - The type of resource.
38
+ * @param resourceId - The ID of the resource.
39
+ * @returns `true` if bookmarked.
40
+ */
41
+ export declare function isBookmarked(userId: string, resourceType: string, resourceId: string): Promise<boolean>;
42
+ /**
43
+ * Gets all unique folder names for a user's bookmarks.
44
+ *
45
+ * @param userId - The user ID.
46
+ * @returns Array of folder names.
47
+ */
48
+ export declare function getFolders(userId: string): Promise<string[]>;
49
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAI1E;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,QAAQ,CAAC,CAgBnB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAuBpC;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWlE"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Bookmark business logic service.
3
+ *
4
+ * @module
5
+ */
6
+ import { count, create as dbCreate, deleteMany, findMany, findOne } from '@molecule/api-database';
7
+ const TABLE = 'bookmarks';
8
+ /**
9
+ * Adds a bookmark. Idempotent — returns existing bookmark if already bookmarked.
10
+ *
11
+ * @param userId - The user ID.
12
+ * @param resourceType - The type of resource to bookmark.
13
+ * @param resourceId - The ID of the resource to bookmark.
14
+ * @param folder - Optional folder name.
15
+ * @returns The created or existing bookmark.
16
+ */
17
+ export async function addBookmark(userId, resourceType, resourceId, folder) {
18
+ const existing = await findOne(TABLE, [
19
+ { field: 'userId', operator: '=', value: userId },
20
+ { field: 'resourceType', operator: '=', value: resourceType },
21
+ { field: 'resourceId', operator: '=', value: resourceId },
22
+ ]);
23
+ if (existing)
24
+ return existing;
25
+ const result = await dbCreate(TABLE, {
26
+ userId,
27
+ resourceType,
28
+ resourceId,
29
+ folder: folder ?? null,
30
+ });
31
+ return result.data;
32
+ }
33
+ /**
34
+ * Removes a bookmark.
35
+ *
36
+ * @param userId - The user ID.
37
+ * @param resourceType - The type of resource.
38
+ * @param resourceId - The ID of the resource.
39
+ */
40
+ export async function removeBookmark(userId, resourceType, resourceId) {
41
+ await deleteMany(TABLE, [
42
+ { field: 'userId', operator: '=', value: userId },
43
+ { field: 'resourceType', operator: '=', value: resourceType },
44
+ { field: 'resourceId', operator: '=', value: resourceId },
45
+ ]);
46
+ }
47
+ /**
48
+ * Gets all bookmarks for a user with optional filtering and pagination.
49
+ *
50
+ * @param userId - The user ID.
51
+ * @param options - Query options.
52
+ * @returns Paginated bookmarks.
53
+ */
54
+ export async function getBookmarks(userId, options = {}) {
55
+ const limit = options.limit ?? 20;
56
+ const offset = options.offset ?? 0;
57
+ const where = [
58
+ { field: 'userId', operator: '=', value: userId },
59
+ ...(options.resourceType
60
+ ? [{ field: 'resourceType', operator: '=', value: options.resourceType }]
61
+ : []),
62
+ ...(options.folder ? [{ field: 'folder', operator: '=', value: options.folder }] : []),
63
+ ];
64
+ const [data, total] = await Promise.all([
65
+ findMany(TABLE, {
66
+ where,
67
+ orderBy: [{ field: 'createdAt', direction: 'desc' }],
68
+ limit,
69
+ offset,
70
+ }),
71
+ count(TABLE, where),
72
+ ]);
73
+ return { data, total, limit, offset };
74
+ }
75
+ /**
76
+ * Checks if a resource is bookmarked by a user.
77
+ *
78
+ * @param userId - The user ID.
79
+ * @param resourceType - The type of resource.
80
+ * @param resourceId - The ID of the resource.
81
+ * @returns `true` if bookmarked.
82
+ */
83
+ export async function isBookmarked(userId, resourceType, resourceId) {
84
+ const existing = await findOne(TABLE, [
85
+ { field: 'userId', operator: '=', value: userId },
86
+ { field: 'resourceType', operator: '=', value: resourceType },
87
+ { field: 'resourceId', operator: '=', value: resourceId },
88
+ ]);
89
+ return existing !== null;
90
+ }
91
+ /**
92
+ * Gets all unique folder names for a user's bookmarks.
93
+ *
94
+ * @param userId - The user ID.
95
+ * @returns Array of folder names.
96
+ */
97
+ export async function getFolders(userId) {
98
+ const bookmarks = await findMany(TABLE, {
99
+ where: [{ field: 'userId', operator: '=', value: userId }],
100
+ select: ['folder'],
101
+ });
102
+ const folders = new Set();
103
+ for (const b of bookmarks) {
104
+ if (b.folder)
105
+ folders.add(b.folder);
106
+ }
107
+ return [...folders].sort();
108
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Bookmark resource type definitions.
3
+ *
4
+ * User bookmarks that can reference any resource type. Supports optional
5
+ * folder organization.
6
+ *
7
+ * @module
8
+ */
9
+ /**
10
+ * A bookmark linking a user to a resource, with optional folder grouping.
11
+ */
12
+ export interface Bookmark {
13
+ /** Unique bookmark identifier. */
14
+ id: string;
15
+ /** The ID of the user who created the bookmark. */
16
+ userId: string;
17
+ /** The type of resource bookmarked (e.g. 'post', 'project'). */
18
+ resourceType: string;
19
+ /** The ID of the bookmarked resource. */
20
+ resourceId: string;
21
+ /** Optional folder name for organizing bookmarks. */
22
+ folder: string | null;
23
+ /** When the bookmark was created (ISO 8601). */
24
+ createdAt: string;
25
+ /** When the bookmark was last updated (ISO 8601). */
26
+ updatedAt: string;
27
+ }
28
+ /**
29
+ * Query options for listing bookmarks.
30
+ */
31
+ export interface BookmarkQuery {
32
+ /** Filter by resource type. */
33
+ resourceType?: string;
34
+ /** Filter by folder name. */
35
+ folder?: string;
36
+ /** Maximum number of results to return. */
37
+ limit?: number;
38
+ /** Number of results to skip. */
39
+ offset?: number;
40
+ }
41
+ /**
42
+ * A paginated result set.
43
+ */
44
+ export interface PaginatedResult<T> {
45
+ /** The result items for the current page. */
46
+ data: T[];
47
+ /** Total number of matching items across all pages. */
48
+ total: number;
49
+ /** Maximum number of results per page. */
50
+ limit: number;
51
+ /** Number of results skipped. */
52
+ offset: number;
53
+ }
54
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAA;IACpB,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,qDAAqD;IACrD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAA;IACjB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,6CAA6C;IAC7C,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAA;CACf"}
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Bookmark resource type definitions.
3
+ *
4
+ * User bookmarks that can reference any resource type. Supports optional
5
+ * folder organization.
6
+ *
7
+ * @module
8
+ */
9
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Bookmark input validation schemas.
3
+ *
4
+ * @module
5
+ */
6
+ import { z } from 'zod';
7
+ /**
8
+ * Schema for validating bookmark creation input.
9
+ */
10
+ export declare const createBookmarkSchema: z.ZodObject<{
11
+ resourceType: z.ZodString;
12
+ resourceId: z.ZodString;
13
+ folder: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$strip>;
15
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;;iBAO/B,CAAA"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Bookmark input validation schemas.
3
+ *
4
+ * @module
5
+ */
6
+ import { z } from 'zod';
7
+ /**
8
+ * Schema for validating bookmark creation input.
9
+ */
10
+ export const createBookmarkSchema = z.object({
11
+ /** The type of resource to bookmark. */
12
+ resourceType: z.string().min(1).max(255),
13
+ /** The ID of the resource to bookmark. */
14
+ resourceId: z.string().min(1).max(255),
15
+ /** Optional folder name for organization. */
16
+ folder: z.string().min(1).max(255).optional(),
17
+ });
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@molecule/api-resource-bookmark",
3
+ "version": "1.0.0",
4
+ "description": "Bookmark/favorite any resource with folder organization",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "molecule",
24
+ "bookmark",
25
+ "favorite",
26
+ "resource"
27
+ ],
28
+ "license": "Apache-2.0",
29
+ "devDependencies": {
30
+ "@types/node": "26.1.2",
31
+ "typescript": "6.0.3",
32
+ "vitest": "4.1.10",
33
+ "zod": "4.4.3"
34
+ },
35
+ "peerDependencies": {
36
+ "@molecule/api-database": "^1.0.0",
37
+ "@molecule/api-i18n": "^1.0.0",
38
+ "@molecule/api-logger": "^1.0.0",
39
+ "@molecule/api-resource": "^1.0.0",
40
+ "zod": "^4.0.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@molecule/api-database": {
44
+ "optional": true
45
+ },
46
+ "@molecule/api-i18n": {
47
+ "optional": true
48
+ },
49
+ "@molecule/api-logger": {
50
+ "optional": true
51
+ },
52
+ "@molecule/api-resource": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/molecule-dev/molecule.git",
59
+ "directory": "packages/api/resources/bookmark"
60
+ },
61
+ "homepage": "https://github.com/molecule-dev/molecule/tree/main/packages/api/resources/bookmark",
62
+ "bugs": "https://github.com/molecule-dev/molecule/issues",
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }