@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.
- package/LICENSE +115 -0
- package/dist/authorizers/index.d.ts +10 -0
- package/dist/authorizers/index.d.ts.map +1 -0
- package/dist/authorizers/index.js +9 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/handlers/create.d.ts +14 -0
- package/dist/handlers/create.d.ts.map +1 -0
- package/dist/handlers/create.js +44 -0
- package/dist/handlers/del.d.ts +14 -0
- package/dist/handlers/del.d.ts.map +1 -0
- package/dist/handlers/del.js +47 -0
- package/dist/handlers/index.d.ts +6 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +5 -0
- package/dist/handlers/list.d.ts +14 -0
- package/dist/handlers/list.d.ts.map +1 -0
- package/dist/handlers/list.js +41 -0
- package/dist/handlers/read.d.ts +14 -0
- package/dist/handlers/read.d.ts.map +1 -0
- package/dist/handlers/read.js +47 -0
- package/dist/handlers/update.d.ts +14 -0
- package/dist/handlers/update.d.ts.map +1 -0
- package/dist/handlers/update.js +37 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/requestHandlerMap.d.ts +21 -0
- package/dist/requestHandlerMap.d.ts.map +1 -0
- package/dist/requestHandlerMap.js +20 -0
- package/dist/routes.d.ts +37 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +27 -0
- package/dist/service.d.ts +49 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +108 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/validation.d.ts +15 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +17 -0
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/authorizers/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG"}
|
|
@@ -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 @@
|
|
|
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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/routes.d.ts
ADDED
|
@@ -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"}
|
package/dist/service.js
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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,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
|
+
}
|