@jaypie/mcp 0.1.9 → 0.2.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 (41) hide show
  1. package/dist/datadog.d.ts +212 -0
  2. package/dist/index.js +1461 -6
  3. package/dist/index.js.map +1 -1
  4. package/package.json +10 -7
  5. package/prompts/Development_Process.md +57 -35
  6. package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +143 -19
  7. package/prompts/Jaypie_Express_Package.md +408 -0
  8. package/prompts/Jaypie_Init_Express_on_Lambda.md +66 -38
  9. package/prompts/Jaypie_Init_Lambda_Package.md +202 -83
  10. package/prompts/Jaypie_Init_Project_Subpackage.md +21 -26
  11. package/prompts/Jaypie_Legacy_Patterns.md +4 -0
  12. package/prompts/Templates_CDK_Subpackage.md +113 -0
  13. package/prompts/Templates_Express_Subpackage.md +183 -0
  14. package/prompts/Templates_Project_Monorepo.md +326 -0
  15. package/prompts/Templates_Project_Subpackage.md +93 -0
  16. package/LICENSE.txt +0 -21
  17. package/prompts/Jaypie_Mongoose_Models_Package.md +0 -231
  18. package/prompts/Jaypie_Mongoose_with_Express_CRUD.md +0 -1000
  19. package/prompts/templates/cdk-subpackage/bin/cdk.ts +0 -11
  20. package/prompts/templates/cdk-subpackage/cdk.json +0 -19
  21. package/prompts/templates/cdk-subpackage/lib/cdk-app.ts +0 -41
  22. package/prompts/templates/cdk-subpackage/lib/cdk-infrastructure.ts +0 -15
  23. package/prompts/templates/express-subpackage/index.ts +0 -8
  24. package/prompts/templates/express-subpackage/src/app.ts +0 -18
  25. package/prompts/templates/express-subpackage/src/handler.config.ts +0 -44
  26. package/prompts/templates/express-subpackage/src/routes/resource/__tests__/resourceGet.route.spec.ts +0 -29
  27. package/prompts/templates/express-subpackage/src/routes/resource/resourceGet.route.ts +0 -22
  28. package/prompts/templates/express-subpackage/src/routes/resource.router.ts +0 -11
  29. package/prompts/templates/express-subpackage/src/types/express.ts +0 -9
  30. package/prompts/templates/project-monorepo/.vscode/settings.json +0 -72
  31. package/prompts/templates/project-monorepo/eslint.config.mjs +0 -1
  32. package/prompts/templates/project-monorepo/gitignore +0 -11
  33. package/prompts/templates/project-monorepo/package.json +0 -20
  34. package/prompts/templates/project-monorepo/tsconfig.base.json +0 -18
  35. package/prompts/templates/project-monorepo/tsconfig.json +0 -6
  36. package/prompts/templates/project-monorepo/vitest.workspace.js +0 -3
  37. package/prompts/templates/project-subpackage/package.json +0 -16
  38. package/prompts/templates/project-subpackage/tsconfig.json +0 -11
  39. package/prompts/templates/project-subpackage/vite.config.ts +0 -21
  40. package/prompts/templates/project-subpackage/vitest.config.ts +0 -7
  41. package/prompts/templates/project-subpackage/vitest.setup.ts +0 -6
@@ -1,1000 +0,0 @@
1
- ---
2
- description: MongoDB/Mongoose integration with Jaypie Express patterns
3
- globs: packages/express/**
4
- ---
5
-
6
- # Jaypie Mongoose with Express CRUD
7
-
8
- MongoDB/Mongoose integration patterns for Jaypie Express applications with full CRUD operations, authentication, and relationship management using TypeScript.
9
-
10
- ## Pattern
11
-
12
- - TypeScript with ES modules (`"type": "module"`)
13
- - Model package with MongoDB/Mongoose connections
14
- - Handler configuration with connection lifecycle
15
- - Authentication/authorization with user context
16
- - JSON:API formatting for consistent responses
17
- - Relationship management with foreign keys
18
- - Type-safe interfaces for all data models
19
-
20
- ## Dependencies
21
-
22
- ```json
23
- {
24
- "dependencies": {
25
- "@yourorg/models": "^1.0.0",
26
- "mongoose": "^8.0.0",
27
- "jaypie": "^1.1.0",
28
- "express": "^4.19.0",
29
- "express-oauth2-jwt-bearer": "^1.6.0",
30
- "ajv": "^8.12.0",
31
- "ajv-formats": "^2.1.1"
32
- },
33
- "devDependencies": {
34
- "@types/express": "^4.17.0",
35
- "@types/mongoose": "^8.0.0",
36
- "typescript": "^5.0.0"
37
- }
38
- }
39
- ```
40
-
41
- ## TypeScript Configuration
42
-
43
- **tsconfig.json**
44
- ```json
45
- {
46
- "compilerOptions": {
47
- "target": "ES2022",
48
- "module": "ESNext",
49
- "moduleResolution": "node",
50
- "declaration": true,
51
- "outDir": "./dist",
52
- "strict": true,
53
- "esModuleInterop": true,
54
- "skipLibCheck": true,
55
- "forceConsistentCasingInFileNames": true,
56
- "resolveJsonModule": true,
57
- "allowSyntheticDefaultImports": true
58
- },
59
- "include": ["src/**/*", "index.ts"],
60
- "exclude": ["node_modules", "dist", "**/*.spec.ts"]
61
- }
62
- ```
63
-
64
- ## Handler Configuration
65
-
66
- **src/handler.config.ts** - Database lifecycle management
67
- ```typescript
68
- import { force } from "jaypie";
69
- import Model from "@yourorg/models";
70
- import systemContextLocal from "./util/systemContextLocal.js";
71
- import userLocal from "./util/userLocal.js";
72
- import validateAuth from "./util/validateAuth.js";
73
-
74
- interface HandlerConfigOptions {
75
- locals?: Record<string, any>;
76
- setup?: Array<() => void | Promise<void>>;
77
- teardown?: Array<() => void | Promise<void>>;
78
- validate?: Array<() => boolean | Promise<boolean>>;
79
- }
80
-
81
- interface HandlerConfig {
82
- name: string;
83
- locals: Record<string, any>;
84
- setup: Array<() => void | Promise<void>>;
85
- teardown: Array<() => void | Promise<void>>;
86
- validate: Array<() => boolean | Promise<boolean>>;
87
- }
88
-
89
- const handlerConfig = (
90
- nameOrConfig: string | (HandlerConfig & { name: string }),
91
- options: HandlerConfigOptions = {}
92
- ): HandlerConfig => {
93
- let name: string;
94
- let locals: Record<string, any>;
95
- let setup: Array<() => void | Promise<void>>;
96
- let teardown: Array<() => void | Promise<void>>;
97
- let validate: Array<() => boolean | Promise<boolean>>;
98
-
99
- if (typeof nameOrConfig === "object") {
100
- ({ name, locals = {}, setup = [], teardown = [], validate = [] } = nameOrConfig);
101
- } else {
102
- name = nameOrConfig;
103
- ({ locals = {}, setup = [], teardown = [], validate = [] } = options);
104
- }
105
-
106
- return {
107
- name,
108
- locals: { ...force.object(locals), systemContext: systemContextLocal, user: userLocal },
109
- setup: [Model.connect, ...force.array(setup)],
110
- teardown: [...force.array(teardown), Model.disconnect],
111
- validate: [validateAuth, ...force.array(validate)],
112
- };
113
- };
114
-
115
- export default handlerConfig;
116
- ```
117
-
118
- ## Authentication & Authorization
119
-
120
- **src/util/validateAuth.ts** - JWT validation
121
- ```typescript
122
- import { UnauthorizedError } from "jaypie";
123
- import { auth } from "express-oauth2-jwt-bearer";
124
- import type { Request, Response, NextFunction } from "express";
125
-
126
- const jwtCheck = auth({
127
- audience: process.env.AUTH0_AUDIENCE,
128
- issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL,
129
- });
130
-
131
- export default async (req: Request, res: Response, next: NextFunction): Promise<void> => {
132
- return new Promise((resolve, reject) => {
133
- jwtCheck(req, res, (err) => {
134
- if (err) {
135
- reject(new UnauthorizedError("Invalid token"));
136
- } else {
137
- resolve();
138
- }
139
- });
140
- });
141
- };
142
- ```
143
-
144
- **src/util/userLocal.ts** - User context extraction
145
- ```typescript
146
- import { UnauthorizedError } from "jaypie";
147
- import type { Request } from "express";
148
-
149
- interface UserContext {
150
- sub: string;
151
- email: string;
152
- groups: string[];
153
- portfolios: string[];
154
- }
155
-
156
- export default async (req: Request): Promise<UserContext> => {
157
- const user = req.auth?.payload;
158
- if (!user) {
159
- throw new UnauthorizedError("User not authenticated");
160
- }
161
-
162
- return {
163
- sub: user.sub,
164
- email: user.email,
165
- groups: user.groups || [],
166
- portfolios: user.portfolios || [],
167
- };
168
- };
169
- ```
170
-
171
- **src/util/authUserHasGroups.ts** - Group authorization
172
- ```typescript
173
- import { ForbiddenError } from "jaypie";
174
-
175
- interface AuthUserHasGroupsOptions {
176
- requireGroups?: boolean;
177
- }
178
-
179
- interface User {
180
- groups?: string[];
181
- }
182
-
183
- export default (user: User, options: AuthUserHasGroupsOptions = {}): boolean => {
184
- const { requireGroups = true } = options;
185
-
186
- if (requireGroups && (!user.groups || user.groups.length === 0)) {
187
- throw new ForbiddenError("User must belong to at least one group");
188
- }
189
-
190
- return true;
191
- };
192
- ```
193
-
194
- **src/util/authUserHasAllGroups.ts** - Specific group validation
195
- ```typescript
196
- import { ForbiddenError } from "jaypie";
197
-
198
- interface AuthUserHasAllGroupsOptions {
199
- groupUuids: string[];
200
- }
201
-
202
- interface User {
203
- groups?: string[];
204
- }
205
-
206
- export default async (user: User, { groupUuids }: AuthUserHasAllGroupsOptions): Promise<boolean> => {
207
- const userGroups = user.groups || [];
208
- const hasAllGroups = groupUuids.every(groupUuid =>
209
- userGroups.includes(groupUuid)
210
- );
211
-
212
- if (!hasAllGroups) {
213
- throw new ForbiddenError("User lacks required group permissions");
214
- }
215
-
216
- return true;
217
- };
218
- ```
219
-
220
- ## Database Operations
221
-
222
- ### Model Connection
223
- ```typescript
224
- // Connect to MongoDB
225
- await Model.connect();
226
-
227
- // Disconnect from MongoDB
228
- await Model.disconnect();
229
- ```
230
-
231
- ### CRUD Operations
232
-
233
- **Create with relationships**
234
- ```typescript
235
- import Model from "@yourorg/models";
236
-
237
- const Resource = Model.Resource;
238
- const Group = Model.Group;
239
-
240
- await Resource.createWithRelationships({
241
- foreignKey: "resources",
242
- models: { groups: Group },
243
- uuid: resourceId,
244
- values: {
245
- name: "Resource Name",
246
- description: "Resource Description",
247
- groups: ["group-uuid-1", "group-uuid-2"]
248
- },
249
- });
250
- ```
251
-
252
- **Read operations**
253
- ```typescript
254
- // Find by UUID
255
- const resource = await Model.Resource.oneByUuid(resourceId);
256
-
257
- // Find by query
258
- const resources = await Model.Resource.find({
259
- name: { $regex: "search-term", $options: "i" }
260
- });
261
-
262
- // Find with relationships
263
- const resource = await Model.Resource.oneByUuid(resourceId, {
264
- populate: ["groups", "portfolios"]
265
- });
266
- ```
267
-
268
- **Update with relationships**
269
- ```typescript
270
- await Model.Resource.updateWithRelationships({
271
- uuid: resourceId,
272
- values: {
273
- name: "Updated Name",
274
- groups: ["new-group-uuid"]
275
- },
276
- });
277
- ```
278
-
279
- **Delete operations**
280
- ```typescript
281
- // Soft delete
282
- await Model.Resource.deleteByUuid(resourceId);
283
-
284
- // Hard delete
285
- await Model.Resource.destroyByUuid(resourceId);
286
- ```
287
-
288
- ## CRUD Route Handlers
289
-
290
- **src/routes/resource/resourceCreate.route.ts**
291
- ```typescript
292
- import { cloneDeep, expressHandler, uuid } from "jaypie";
293
- import Model from "@yourorg/models";
294
- import handlerConfig from "../../handler.config.js";
295
- import authUserHasGroups from "../../util/authUserHasGroups.js";
296
- import formatMongoToJsonApi from "../../util/formatMongoToJsonApi.js";
297
- import jsonApiValidator from "../../util/jsonApiValidator.js";
298
- import resourceJsonSchema from "./resourceJson.schema.js";
299
- import type { Request } from "express";
300
-
301
- interface ResourceCreateRequest extends Request {
302
- body: {
303
- data: {
304
- attributes: {
305
- name: string;
306
- description?: string;
307
- groups?: string[];
308
- portfolios?: string[];
309
- };
310
- };
311
- };
312
- locals: {
313
- user: {
314
- groups: string[];
315
- };
316
- };
317
- }
318
-
319
- interface ResourceCreateResponse {
320
- data: {
321
- type: string;
322
- id: string;
323
- attributes: Record<string, any>;
324
- };
325
- }
326
-
327
- export default expressHandler(
328
- handlerConfig({
329
- name: "resourceCreate",
330
- validate: jsonApiValidator(resourceJsonSchema),
331
- }),
332
- async (req: ResourceCreateRequest): Promise<ResourceCreateResponse> => {
333
- const { user } = req.locals;
334
-
335
- authUserHasGroups(user);
336
-
337
- const Resource = Model.Resource;
338
- const Group = Model.Group;
339
- const id = uuid();
340
-
341
- const { groups = [] } = req.body.data.attributes;
342
- if (!groups.length) {
343
- groups.push(...(await Group.idsToUuids(user.groups)));
344
- }
345
-
346
- const createdResource = Object.assign(
347
- { name: "Default Resource" },
348
- cloneDeep(req.body.data.attributes),
349
- { groups }
350
- );
351
-
352
- await Resource.createWithRelationships({
353
- foreignKey: "resources",
354
- models: { groups: Group },
355
- uuid: id,
356
- values: createdResource,
357
- });
358
-
359
- const returnResource = cloneDeep(createdResource);
360
- returnResource.uuid = id;
361
- return formatMongoToJsonApi(returnResource, resourceJsonSchema);
362
- }
363
- );
364
- ```
365
-
366
- **src/routes/resource/resourceRead.route.ts**
367
- ```typescript
368
- import { expressHandler, NotFoundError } from "jaypie";
369
- import Model from "@yourorg/models";
370
- import handlerConfig from "../../handler.config.js";
371
- import authUserHasGroups from "../../util/authUserHasGroups.js";
372
- import formatMongoToJsonApi from "../../util/formatMongoToJsonApi.js";
373
- import resourceJsonSchema from "./resourceJson.schema.js";
374
- import type { Request } from "express";
375
-
376
- interface ResourceReadRequest extends Request {
377
- params: {
378
- id: string;
379
- };
380
- locals: {
381
- user: {
382
- groups: string[];
383
- };
384
- };
385
- }
386
-
387
- interface ResourceReadResponse {
388
- data: {
389
- type: string;
390
- id: string;
391
- attributes: Record<string, any>;
392
- };
393
- }
394
-
395
- export default expressHandler(
396
- handlerConfig("resourceRead"),
397
- async (req: ResourceReadRequest): Promise<ResourceReadResponse> => {
398
- const { user } = req.locals;
399
- const { id } = req.params;
400
-
401
- authUserHasGroups(user);
402
-
403
- const Resource = Model.Resource;
404
- const resource = await Resource.oneByUuid(id);
405
-
406
- if (!resource) {
407
- throw new NotFoundError("Resource not found");
408
- }
409
-
410
- // Check user has access to resource groups
411
- const hasAccess = resource.groups.some((group: string) =>
412
- user.groups.includes(group)
413
- );
414
-
415
- if (!hasAccess) {
416
- throw new NotFoundError("Resource not found");
417
- }
418
-
419
- return formatMongoToJsonApi(resource, resourceJsonSchema);
420
- }
421
- );
422
- ```
423
-
424
- **src/routes/resource/resourceUpdate.route.ts**
425
- ```typescript
426
- import { cloneDeep, expressHandler, NotFoundError } from "jaypie";
427
- import Model from "@yourorg/models";
428
- import handlerConfig from "../../handler.config.js";
429
- import authUserHasGroups from "../../util/authUserHasGroups.js";
430
- import formatMongoToJsonApi from "../../util/formatMongoToJsonApi.js";
431
- import jsonApiValidator from "../../util/jsonApiValidator.js";
432
- import resourceJsonSchema from "./resourceJson.schema.js";
433
- import type { Request } from "express";
434
-
435
- interface ResourceUpdateRequest extends Request {
436
- body: {
437
- data: {
438
- attributes: Record<string, any>;
439
- };
440
- };
441
- params: {
442
- id: string;
443
- };
444
- locals: {
445
- user: {
446
- groups: string[];
447
- };
448
- };
449
- }
450
-
451
- interface ResourceUpdateResponse {
452
- data: {
453
- type: string;
454
- id: string;
455
- attributes: Record<string, any>;
456
- };
457
- }
458
-
459
- export default expressHandler(
460
- handlerConfig({
461
- name: "resourceUpdate",
462
- validate: jsonApiValidator(resourceJsonSchema),
463
- }),
464
- async (req: ResourceUpdateRequest): Promise<ResourceUpdateResponse> => {
465
- const { user } = req.locals;
466
- const { id } = req.params;
467
-
468
- authUserHasGroups(user);
469
-
470
- const Resource = Model.Resource;
471
- const existingResource = await Resource.oneByUuid(id);
472
-
473
- if (!existingResource) {
474
- throw new NotFoundError("Resource not found");
475
- }
476
-
477
- const updateData = cloneDeep(req.body.data.attributes);
478
-
479
- await Resource.updateWithRelationships({
480
- uuid: id,
481
- values: updateData,
482
- });
483
-
484
- const updatedResource = await Resource.oneByUuid(id);
485
- return formatMongoToJsonApi(updatedResource, resourceJsonSchema);
486
- }
487
- );
488
- ```
489
-
490
- **src/routes/resource/resourceDelete.route.ts**
491
- ```typescript
492
- import { expressHandler, NotFoundError } from "jaypie";
493
- import Model from "@yourorg/models";
494
- import handlerConfig from "../../handler.config.js";
495
- import authUserHasGroups from "../../util/authUserHasGroups.js";
496
- import type { Request } from "express";
497
-
498
- interface ResourceDeleteRequest extends Request {
499
- params: {
500
- id: string;
501
- };
502
- locals: {
503
- user: {
504
- groups: string[];
505
- };
506
- };
507
- }
508
-
509
- export default expressHandler(
510
- handlerConfig("resourceDelete"),
511
- async (req: ResourceDeleteRequest): Promise<boolean> => {
512
- const { user } = req.locals;
513
- const { id } = req.params;
514
-
515
- authUserHasGroups(user);
516
-
517
- const Resource = Model.Resource;
518
- const resource = await Resource.oneByUuid(id);
519
-
520
- if (!resource) {
521
- throw new NotFoundError("Resource not found");
522
- }
523
-
524
- await Resource.deleteByUuid(id);
525
- return true; // 201 Created
526
- }
527
- );
528
- ```
529
-
530
- **src/routes/resource/resourceIndex.route.ts**
531
- ```typescript
532
- import { expressHandler } from "jaypie";
533
- import Model from "@yourorg/models";
534
- import handlerConfig from "../../handler.config.js";
535
- import authUserHasGroups from "../../util/authUserHasGroups.js";
536
- import formatMongoToJsonApi from "../../util/formatMongoToJsonApi.js";
537
- import resourceJsonSchema from "./resourceJson.schema.js";
538
- import type { Request } from "express";
539
-
540
- interface ResourceIndexRequest extends Request {
541
- query: {
542
- limit?: string;
543
- offset?: string;
544
- search?: string;
545
- };
546
- locals: {
547
- user: {
548
- groups: string[];
549
- };
550
- };
551
- }
552
-
553
- interface ResourceIndexResponse {
554
- data: Array<{
555
- type: string;
556
- id: string;
557
- attributes: Record<string, any>;
558
- }>;
559
- meta: {
560
- total: number;
561
- limit: number;
562
- offset: number;
563
- };
564
- }
565
-
566
- export default expressHandler(
567
- handlerConfig("resourceIndex"),
568
- async (req: ResourceIndexRequest): Promise<ResourceIndexResponse> => {
569
- const { user } = req.locals;
570
- const { limit = "20", offset = "0", search } = req.query;
571
-
572
- authUserHasGroups(user);
573
-
574
- const Resource = Model.Resource;
575
- let query: Record<string, any> = {
576
- groups: { $in: user.groups }
577
- };
578
-
579
- if (search) {
580
- query.name = { $regex: search, $options: "i" };
581
- }
582
-
583
- const resources = await Resource.find(query)
584
- .limit(parseInt(limit))
585
- .skip(parseInt(offset))
586
- .sort({ createdAt: -1 });
587
-
588
- const total = await Resource.countDocuments(query);
589
-
590
- return {
591
- data: resources.map(resource =>
592
- formatMongoToJsonApi(resource, resourceJsonSchema)
593
- ),
594
- meta: {
595
- total,
596
- limit: parseInt(limit),
597
- offset: parseInt(offset),
598
- },
599
- };
600
- }
601
- );
602
- ```
603
-
604
- ## JSON Schema & Validation
605
-
606
- **src/routes/resource/resourceJson.schema.ts**
607
- ```typescript
608
- interface ResourceJsonSchema {
609
- type: string;
610
- properties: {
611
- data: {
612
- type: string;
613
- properties: {
614
- type: {
615
- type: string;
616
- enum: string[];
617
- };
618
- attributes: {
619
- type: string;
620
- properties: {
621
- name: {
622
- type: string;
623
- minLength: number;
624
- maxLength: number;
625
- };
626
- description: {
627
- type: string;
628
- maxLength: number;
629
- };
630
- groups: {
631
- type: string;
632
- items: {
633
- type: string;
634
- format: string;
635
- };
636
- };
637
- portfolios: {
638
- type: string;
639
- items: {
640
- type: string;
641
- format: string;
642
- };
643
- };
644
- };
645
- required: string[];
646
- additionalProperties: boolean;
647
- };
648
- };
649
- required: string[];
650
- additionalProperties: boolean;
651
- };
652
- };
653
- required: string[];
654
- additionalProperties: boolean;
655
- }
656
-
657
- const resourceJsonSchema: ResourceJsonSchema = {
658
- type: "object",
659
- properties: {
660
- data: {
661
- type: "object",
662
- properties: {
663
- type: {
664
- type: "string",
665
- enum: ["resource"],
666
- },
667
- attributes: {
668
- type: "object",
669
- properties: {
670
- name: {
671
- type: "string",
672
- minLength: 1,
673
- maxLength: 255,
674
- },
675
- description: {
676
- type: "string",
677
- maxLength: 1000,
678
- },
679
- groups: {
680
- type: "array",
681
- items: {
682
- type: "string",
683
- format: "uuid",
684
- },
685
- },
686
- portfolios: {
687
- type: "array",
688
- items: {
689
- type: "string",
690
- format: "uuid",
691
- },
692
- },
693
- },
694
- required: ["name"],
695
- additionalProperties: false,
696
- },
697
- },
698
- required: ["type", "attributes"],
699
- additionalProperties: false,
700
- },
701
- },
702
- required: ["data"],
703
- additionalProperties: false,
704
- };
705
-
706
- export default resourceJsonSchema;
707
- ```
708
-
709
- ## Utility Functions
710
-
711
- **src/util/formatMongoToJsonApi.ts**
712
- ```typescript
713
- interface MongoDoc {
714
- toObject?: () => Record<string, any>;
715
- [key: string]: any;
716
- }
717
-
718
- interface JsonApiSchema {
719
- properties: {
720
- data: {
721
- properties: {
722
- type: {
723
- enum: string[];
724
- };
725
- };
726
- };
727
- };
728
- }
729
-
730
- interface JsonApiResponse {
731
- data: {
732
- type: string;
733
- id: string;
734
- attributes: Record<string, any>;
735
- };
736
- }
737
-
738
- export default (mongoDoc: MongoDoc, schema: JsonApiSchema): JsonApiResponse => {
739
- const { _id, __v, ...attributes } = mongoDoc.toObject ? mongoDoc.toObject() : mongoDoc;
740
-
741
- return {
742
- data: {
743
- type: schema.properties.data.properties.type.enum[0],
744
- id: attributes.uuid,
745
- attributes: {
746
- ...attributes,
747
- id: undefined,
748
- uuid: undefined,
749
- },
750
- },
751
- };
752
- };
753
- ```
754
-
755
- **src/util/jsonApiValidator.ts**
756
- ```typescript
757
- import Ajv from "ajv";
758
- import addFormats from "ajv-formats";
759
- import { BadRequestError } from "jaypie";
760
- import type { Request } from "express";
761
-
762
- interface ValidationError {
763
- field: string;
764
- message: string;
765
- }
766
-
767
- const ajv = new Ajv({ allErrors: true });
768
- addFormats(ajv);
769
-
770
- export default (schema: object) => {
771
- const validate = ajv.compile(schema);
772
-
773
- return (req: Request): boolean => {
774
- const valid = validate(req.body);
775
-
776
- if (!valid) {
777
- const errors: ValidationError[] = validate.errors?.map(error => ({
778
- field: error.instancePath,
779
- message: error.message || "Validation error",
780
- })) || [];
781
-
782
- throw new BadRequestError("Validation failed", { errors });
783
- }
784
-
785
- return true;
786
- };
787
- };
788
- ```
789
-
790
- ## Testing Setup
791
-
792
- **vitest.config.ts**
793
- ```typescript
794
- import { defineConfig } from "vitest/config";
795
-
796
- export default defineConfig({
797
- test: {
798
- setupFiles: ["./vitest.setup.ts"],
799
- },
800
- });
801
- ```
802
-
803
- **vitest.setup.ts** - MongoDB mocking
804
- ```typescript
805
- import Model from "@yourorg/models";
806
- import { matchers as jaypieMatchers } from "@jaypie/testkit";
807
- import * as extendedMatchers from "jest-extended";
808
- import { expect, vi } from "vitest";
809
-
810
- expect.extend(extendedMatchers);
811
- expect.extend(jaypieMatchers);
812
-
813
- vi.mock("@yourorg/models");
814
- vi.mock("jaypie", async () => vi.importActual("@jaypie/testkit/mock"));
815
- vi.mock("./src/util/userLocal.js");
816
- vi.mock("./src/util/validateAuth.js");
817
-
818
- // Mock Model methods
819
- Model.connect = vi.fn();
820
- Model.disconnect = vi.fn();
821
-
822
- Model.mock.Resource.oneByUuid.mockResolvedValue({
823
- _id: "resource000000000000000",
824
- name: "Mock Resource",
825
- uuid: "MOCK_RESOURCE_UUID",
826
- groups: ["group000000000000000"],
827
- toObject: () => ({
828
- name: "Mock Resource",
829
- uuid: "MOCK_RESOURCE_UUID",
830
- groups: ["group000000000000000"],
831
- }),
832
- });
833
-
834
- Model.mock.Resource.find.mockResolvedValue([
835
- {
836
- _id: "resource000000000000001",
837
- name: "Mock Resource 1",
838
- uuid: "MOCK_RESOURCE_UUID_1",
839
- groups: ["group000000000000000"],
840
- },
841
- ]);
842
-
843
- Model.mock.Resource.createWithRelationships.mockResolvedValue({
844
- _id: "resource000000000000002",
845
- name: "Created Resource",
846
- uuid: "CREATED_RESOURCE_UUID",
847
- });
848
-
849
- Model.mock.Group.idsToUuids.mockResolvedValue([
850
- "group000000000000000",
851
- ]);
852
- ```
853
-
854
- ## Test Examples
855
-
856
- **Unit Test**
857
- ```typescript
858
- import { uuid } from "jaypie";
859
- import { describe, expect, it } from "vitest";
860
- import Model from "@yourorg/models";
861
- import resourceCreate from "../resourceCreate.route.js";
862
-
863
- uuid.mockReturnValue("generated-uuid");
864
-
865
- describe("Resource Create Route", () => {
866
- it("creates resource with relationships", async () => {
867
- const mockRequest = {
868
- body: {
869
- data: {
870
- type: "resource",
871
- attributes: {
872
- name: "Test Resource",
873
- groups: ["00000000-0000-0000-0000-000000000000"],
874
- },
875
- },
876
- },
877
- locals: {
878
- user: {
879
- sub: "user123",
880
- groups: ["00000000-0000-0000-0000-000000000000"],
881
- },
882
- },
883
- } as any;
884
-
885
- const response = await resourceCreate(mockRequest);
886
-
887
- expect(Model.Resource.createWithRelationships).toHaveBeenCalledWith({
888
- foreignKey: "resources",
889
- models: { groups: Model.Group },
890
- uuid: "generated-uuid",
891
- values: expect.objectContaining({
892
- name: "Test Resource",
893
- groups: ["00000000-0000-0000-0000-000000000000"],
894
- }),
895
- });
896
-
897
- expect(response.data.id).toBe("generated-uuid");
898
- });
899
-
900
- it("throws when user has no groups", async () => {
901
- const mockRequest = {
902
- body: {
903
- data: {
904
- type: "resource",
905
- attributes: { name: "Test Resource" },
906
- },
907
- },
908
- locals: {
909
- user: { sub: "user123", groups: [] },
910
- },
911
- } as any;
912
-
913
- await expect(() => resourceCreate(mockRequest)).toThrowForbiddenError();
914
- });
915
- });
916
- ```
917
-
918
- ## Connection Management
919
-
920
- ### Environment Variables
921
- ```bash
922
- MONGODB_URI=mongodb://localhost:27017/myapp
923
- MONGODB_DB_NAME=myapp
924
- ```
925
-
926
- ### Model Configuration
927
- ```typescript
928
- // @yourorg/models package structure
929
- export default {
930
- connect: async () => {
931
- await mongoose.connect(process.env.MONGODB_URI);
932
- },
933
- disconnect: async () => {
934
- await mongoose.disconnect();
935
- },
936
- Resource: ResourceModel,
937
- Group: GroupModel,
938
- Portfolio: PortfolioModel,
939
- };
940
- ```
941
-
942
- ## Error Handling
943
-
944
- ### Database Errors
945
- ```typescript
946
- import { InternalError, BadRequestError } from "jaypie";
947
-
948
- try {
949
- await Model.Resource.createWithRelationships(data);
950
- } catch (error: any) {
951
- if (error.code === 11000) {
952
- // Duplicate key error
953
- throw new BadRequestError("Resource already exists");
954
- }
955
- throw new InternalError("Database operation failed");
956
- }
957
- ```
958
-
959
- ### Validation Errors
960
- ```typescript
961
- // JSON Schema validation automatically throws BadRequestError
962
- // Custom validation in routes:
963
- if (!resourceData.name || resourceData.name.trim() === "") {
964
- throw new BadRequestError("Resource name is required");
965
- }
966
- ```
967
-
968
- ## Performance Considerations
969
-
970
- ### Indexing
971
- ```typescript
972
- // In your MongoDB models
973
- resourceSchema.index({ uuid: 1 }, { unique: true });
974
- resourceSchema.index({ groups: 1 });
975
- resourceSchema.index({ name: "text" });
976
- ```
977
-
978
- ### Query Optimization
979
- ```typescript
980
- // Use projection to limit fields
981
- const resources = await Resource.find(query, {
982
- name: 1,
983
- uuid: 1,
984
- groups: 1,
985
- createdAt: 1,
986
- });
987
-
988
- // Use lean() for read-only operations
989
- const resources = await Resource.find(query).lean();
990
- ```
991
-
992
- ### Connection Pooling
993
- ```typescript
994
- // In Model.connect()
995
- await mongoose.connect(process.env.MONGODB_URI, {
996
- maxPoolSize: 10,
997
- serverSelectionTimeoutMS: 5000,
998
- socketTimeoutMS: 45000,
999
- });
1000
- ```