@terreno/api 0.13.3 → 0.14.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/dist/__tests__/versionCheckPlugin.test.js +53 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.d.ts +15 -4
- package/dist/api.errors.test.js +1 -0
- package/dist/api.hooks.test.js +1 -0
- package/dist/api.js +153 -104
- package/dist/api.query.test.js +1 -0
- package/dist/api.test.js +174 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +163 -90
- package/dist/auth.test.js +159 -0
- package/dist/betterAuthApp.test.js +1 -0
- package/dist/betterAuthSetup.d.ts +5 -6
- package/dist/betterAuthSetup.js +17 -14
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +248 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +328 -0
- package/dist/configuration.test.js +1 -0
- package/dist/configurationApp.d.ts +1 -1
- package/dist/configurationApp.js +17 -13
- package/dist/configurationPlugin.test.js +1 -0
- package/dist/consentApp.test.js +1 -0
- package/dist/envConfigurationPlugin.d.ts +2 -0
- package/dist/envConfigurationPlugin.js +173 -0
- package/dist/envConfigurationPlugin.test.d.ts +1 -0
- package/dist/envConfigurationPlugin.test.js +322 -0
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +106 -10
- package/dist/errors.test.js +16 -1
- package/dist/example.js +16 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +53 -2
- package/dist/githubAuth.d.ts +2 -1
- package/dist/githubAuth.js +41 -26
- package/dist/githubAuth.test.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +42 -20
- package/dist/models/versionConfig.d.ts +2 -0
- package/dist/models/versionConfig.js +8 -0
- package/dist/notifiers/googleChatNotifier.js +14 -16
- package/dist/notifiers/googleChatNotifier.test.js +1 -0
- package/dist/notifiers/slackNotifier.js +16 -14
- package/dist/notifiers/slackNotifier.test.js +41 -3
- package/dist/notifiers/zoomNotifier.js +7 -10
- package/dist/notifiers/zoomNotifier.test.js +1 -0
- package/dist/openApi.d.ts +1 -1
- package/dist/openApi.test.js +1 -0
- package/dist/openApiBuilder.d.ts +39 -6
- package/dist/openApiBuilder.js +1 -31
- package/dist/openApiBuilder.test.js +1 -0
- package/dist/openApiValidator.js +1 -0
- package/dist/openApiValidator.test.js +1 -0
- package/dist/permissions.d.ts +4 -4
- package/dist/permissions.js +67 -65
- package/dist/permissions.middleware.test.js +1 -0
- package/dist/permissions.test.js +1 -0
- package/dist/plugins.d.ts +5 -5
- package/dist/plugins.js +18 -9
- package/dist/plugins.test.js +1 -1
- package/dist/populate.d.ts +15 -8
- package/dist/populate.js +23 -24
- package/dist/populate.test.js +1 -0
- package/dist/realtime/changeStreamWatcher.d.ts +73 -0
- package/dist/realtime/changeStreamWatcher.js +720 -0
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime/index.js +27 -0
- package/dist/realtime/queryMatcher.d.ts +14 -0
- package/dist/realtime/queryMatcher.js +250 -0
- package/dist/realtime/queryStore.d.ts +37 -0
- package/dist/realtime/queryStore.js +195 -0
- package/dist/realtime/realtime.test.d.ts +10 -0
- package/dist/realtime/realtime.test.js +2158 -0
- package/dist/realtime/realtimeApp.d.ts +93 -0
- package/dist/realtime/realtimeApp.js +560 -0
- package/dist/realtime/registry.d.ts +40 -0
- package/dist/realtime/registry.js +38 -0
- package/dist/realtime/socketUser.d.ts +10 -0
- package/dist/realtime/socketUser.js +17 -0
- package/dist/realtime/types.d.ts +100 -0
- package/dist/realtime/types.js +2 -0
- package/dist/requestContext.d.ts +37 -0
- package/dist/requestContext.js +344 -0
- package/dist/requestContext.test.d.ts +1 -0
- package/dist/requestContext.test.js +241 -0
- package/dist/terrenoApp.d.ts +8 -0
- package/dist/terrenoApp.js +50 -13
- package/dist/terrenoApp.test.js +194 -21
- package/dist/terrenoPlugin.d.ts +11 -0
- package/dist/tests/bunSetup.js +1 -0
- package/dist/tests.js +1 -1
- package/dist/transformers.d.ts +2 -2
- package/dist/transformers.js +5 -3
- package/dist/transformers.test.js +90 -0
- package/dist/types/consentResponse.d.ts +6 -3
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +18 -12
- package/package.json +4 -2
- package/src/__tests__/versionCheckPlugin.test.ts +37 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.errors.test.ts +1 -0
- package/src/api.hooks.test.ts +1 -0
- package/src/api.query.test.ts +1 -0
- package/src/api.test.ts +132 -0
- package/src/api.ts +199 -84
- package/src/auth.test.ts +160 -0
- package/src/auth.ts +120 -50
- package/src/betterAuthApp.test.ts +1 -0
- package/src/betterAuthSetup.test.ts +1 -0
- package/src/betterAuthSetup.ts +46 -19
- package/src/config.test.ts +255 -0
- package/src/config.ts +206 -0
- package/src/configuration.test.ts +1 -0
- package/src/configurationApp.ts +59 -24
- package/src/configurationPlugin.test.ts +1 -0
- package/src/consentApp.test.ts +1 -0
- package/src/envConfigurationPlugin.test.ts +143 -0
- package/src/envConfigurationPlugin.ts +100 -0
- package/src/errors.test.ts +19 -1
- package/src/errors.ts +94 -20
- package/src/example.ts +46 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +50 -2
- package/src/expressServer.ts +80 -50
- package/src/githubAuth.test.ts +1 -0
- package/src/githubAuth.ts +59 -38
- package/src/index.ts +4 -0
- package/src/logger.ts +47 -17
- package/src/models/versionConfig.ts +13 -2
- package/src/notifiers/googleChatNotifier.test.ts +1 -0
- package/src/notifiers/googleChatNotifier.ts +7 -9
- package/src/notifiers/slackNotifier.test.ts +29 -3
- package/src/notifiers/slackNotifier.ts +9 -7
- package/src/notifiers/zoomNotifier.test.ts +1 -0
- package/src/notifiers/zoomNotifier.ts +8 -11
- package/src/openApi.test.ts +1 -0
- package/src/openApi.ts +4 -4
- package/src/openApiBuilder.test.ts +1 -0
- package/src/openApiBuilder.ts +14 -11
- package/src/openApiValidator.test.ts +1 -0
- package/src/openApiValidator.ts +3 -2
- package/src/permissions.middleware.test.ts +1 -0
- package/src/permissions.test.ts +1 -0
- package/src/permissions.ts +30 -25
- package/src/plugins.test.ts +1 -1
- package/src/plugins.ts +21 -14
- package/src/populate.test.ts +1 -0
- package/src/populate.ts +44 -36
- package/src/realtime/changeStreamWatcher.ts +568 -0
- package/src/realtime/index.ts +34 -0
- package/src/realtime/queryMatcher.ts +179 -0
- package/src/realtime/queryStore.ts +132 -0
- package/src/realtime/realtime.test.ts +1755 -0
- package/src/realtime/realtimeApp.ts +478 -0
- package/src/realtime/registry.ts +64 -0
- package/src/realtime/socketUser.ts +25 -0
- package/src/realtime/types.ts +112 -0
- package/src/requestContext.test.ts +196 -0
- package/src/requestContext.ts +368 -0
- package/src/terrenoApp.test.ts +137 -11
- package/src/terrenoApp.ts +64 -17
- package/src/terrenoPlugin.ts +12 -0
- package/src/tests/bunSetup.ts +1 -0
- package/src/tests.ts +7 -2
- package/src/transformers.test.ts +70 -2
- package/src/transformers.ts +15 -7
- package/src/types/consentResponse.ts +8 -10
- package/src/versionCheckPlugin.ts +15 -7
package/src/openApi.ts
CHANGED
|
@@ -199,7 +199,7 @@ export function listOpenApiMiddleware<T>(
|
|
|
199
199
|
// Remove _id from queryFields, we handle that above.
|
|
200
200
|
?.filter((field) => field !== "_id")
|
|
201
201
|
.map((field) => {
|
|
202
|
-
const params: {name: string; in: "query"; schema:
|
|
202
|
+
const params: {name: string; in: "query"; schema: Record<string, unknown>}[] = [];
|
|
203
203
|
|
|
204
204
|
// Check for datetime/number to support gt/gte/lt/lte
|
|
205
205
|
if (
|
|
@@ -462,10 +462,10 @@ export function deleteOpenApiMiddleware<T>(
|
|
|
462
462
|
// Useful for endpoints that don't directly map to a model.
|
|
463
463
|
export function readOpenApiMiddleware<T>(
|
|
464
464
|
options: Partial<ModelRouterOptions<T>>,
|
|
465
|
-
properties:
|
|
465
|
+
properties: Record<string, unknown>,
|
|
466
466
|
required: string[],
|
|
467
|
-
queryParameters:
|
|
468
|
-
):
|
|
467
|
+
queryParameters: Array<Record<string, unknown>>
|
|
468
|
+
): express.RequestHandler {
|
|
469
469
|
if (!options.openApi?.path) {
|
|
470
470
|
// Just log this once rather than for each middleware.
|
|
471
471
|
logger.debug(
|
package/src/openApiBuilder.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* router.get("/stats", middleware, statsHandler);
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
+
import type express from "express";
|
|
32
33
|
import merge from "lodash/merge";
|
|
33
34
|
|
|
34
35
|
import type {ModelRouterOptions} from "./api";
|
|
@@ -111,7 +112,7 @@ export interface OpenApiSchemaProperty {
|
|
|
111
112
|
* };
|
|
112
113
|
* ```
|
|
113
114
|
*/
|
|
114
|
-
export
|
|
115
|
+
export interface OpenApiSchema {
|
|
115
116
|
/** The JSON Schema type (typically "object" or "array") */
|
|
116
117
|
type: string;
|
|
117
118
|
/** Property definitions for object types */
|
|
@@ -122,7 +123,8 @@ export type OpenApiSchema = {
|
|
|
122
123
|
items?: OpenApiSchemaProperty;
|
|
123
124
|
/** Schema for additional properties or boolean to allow/disallow them */
|
|
124
125
|
additionalProperties?: OpenApiSchemaProperty | boolean;
|
|
125
|
-
|
|
126
|
+
[key: string]: unknown;
|
|
127
|
+
}
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* Defines a parameter in an OpenAPI operation.
|
|
@@ -246,7 +248,7 @@ interface ValidationConfig {
|
|
|
246
248
|
*/
|
|
247
249
|
export interface OpenApiBuildResult {
|
|
248
250
|
/** The OpenAPI documentation middleware */
|
|
249
|
-
middleware:
|
|
251
|
+
middleware: express.RequestHandler;
|
|
250
252
|
/** Request body schema if defined */
|
|
251
253
|
bodySchema?: Record<string, OpenApiSchemaProperty>;
|
|
252
254
|
/** Query parameter schemas if defined */
|
|
@@ -397,7 +399,7 @@ export class OpenApiMiddlewareBuilder {
|
|
|
397
399
|
* });
|
|
398
400
|
* ```
|
|
399
401
|
*/
|
|
400
|
-
withRequestBody<T extends Record<string,
|
|
402
|
+
withRequestBody<T extends Record<string, unknown>>(
|
|
401
403
|
schema: {
|
|
402
404
|
[K in keyof T]: OpenApiSchemaProperty;
|
|
403
405
|
},
|
|
@@ -454,7 +456,7 @@ export class OpenApiMiddlewareBuilder {
|
|
|
454
456
|
* builder.withResponse(204, "No content");
|
|
455
457
|
* ```
|
|
456
458
|
*/
|
|
457
|
-
withResponse<T extends Record<string,
|
|
459
|
+
withResponse<T extends Record<string, unknown>>(
|
|
458
460
|
statusCode: number,
|
|
459
461
|
schema:
|
|
460
462
|
| {
|
|
@@ -508,7 +510,7 @@ export class OpenApiMiddlewareBuilder {
|
|
|
508
510
|
* }, {description: "List of users"});
|
|
509
511
|
* ```
|
|
510
512
|
*/
|
|
511
|
-
withArrayResponse<T extends Record<string,
|
|
513
|
+
withArrayResponse<T extends Record<string, unknown>>(
|
|
512
514
|
statusCode: number,
|
|
513
515
|
itemSchema: {
|
|
514
516
|
[K in keyof T]: OpenApiSchemaProperty;
|
|
@@ -671,10 +673,10 @@ export class OpenApiMiddlewareBuilder {
|
|
|
671
673
|
* ```
|
|
672
674
|
*/
|
|
673
675
|
buildWithSchemas(): OpenApiBuildResult {
|
|
674
|
-
const noop = (_a
|
|
676
|
+
const noop: express.RequestHandler = (_a, _b, next) => next();
|
|
675
677
|
|
|
676
678
|
// Build the OpenAPI documentation middleware only (no validation middleware)
|
|
677
|
-
let openApiMiddleware:
|
|
679
|
+
let openApiMiddleware: express.RequestHandler = noop;
|
|
678
680
|
if (this.options.openApi?.path) {
|
|
679
681
|
openApiMiddleware = this.options.openApi.path(
|
|
680
682
|
merge(
|
|
@@ -733,11 +735,12 @@ export class OpenApiMiddlewareBuilder {
|
|
|
733
735
|
* router.get("/users/:id", middleware, getUserHandler);
|
|
734
736
|
* ```
|
|
735
737
|
*/
|
|
738
|
+
// biome-ignore lint/suspicious/noExplicitAny: returns either a single RequestHandler or an array depending on validation config — callers spread or invoke
|
|
736
739
|
build(): any {
|
|
737
|
-
const noop = (_a
|
|
740
|
+
const noop: express.RequestHandler = (_a, _b, next) => next();
|
|
738
741
|
|
|
739
742
|
// Build the OpenAPI documentation middleware
|
|
740
|
-
let openApiMiddleware:
|
|
743
|
+
let openApiMiddleware: express.RequestHandler = noop;
|
|
741
744
|
if (this.options.openApi?.path) {
|
|
742
745
|
openApiMiddleware = this.options.openApi.path(
|
|
743
746
|
merge(
|
|
@@ -768,7 +771,7 @@ export class OpenApiMiddlewareBuilder {
|
|
|
768
771
|
}
|
|
769
772
|
|
|
770
773
|
// Build validation middleware
|
|
771
|
-
const validators:
|
|
774
|
+
const validators: express.RequestHandler[] = [openApiMiddleware];
|
|
772
775
|
|
|
773
776
|
// Add body validation if we have a request body schema
|
|
774
777
|
if (this.validationConfig.validateBody && this.requestBodySchema) {
|
package/src/openApiValidator.ts
CHANGED
|
@@ -184,6 +184,7 @@ const getAjvInstance = (): Ajv => {
|
|
|
184
184
|
useDefaults: true,
|
|
185
185
|
validateSchema: false,
|
|
186
186
|
});
|
|
187
|
+
// biome-ignore lint/suspicious/noExplicitAny: ajv-formats has a known type compat issue with AJV instances
|
|
187
188
|
addFormats(instance as any);
|
|
188
189
|
ajvCache.set(key, instance);
|
|
189
190
|
}
|
|
@@ -644,7 +645,7 @@ export const createValidator = (
|
|
|
644
645
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
645
646
|
// Run body validation first
|
|
646
647
|
if (bodyValidator) {
|
|
647
|
-
bodyValidator(req, res, ((err?:
|
|
648
|
+
bodyValidator(req, res, ((err?: unknown) => {
|
|
648
649
|
if (err) {
|
|
649
650
|
next(err);
|
|
650
651
|
return;
|
|
@@ -714,7 +715,7 @@ const m2sOptions = {
|
|
|
714
715
|
*/
|
|
715
716
|
export const getSchemaFromModel = <T>(model: Model<T>): Record<string, OpenApiSchemaProperty> => {
|
|
716
717
|
const modelSwagger = m2s(model, m2sOptions);
|
|
717
|
-
fixMixedFields(
|
|
718
|
+
fixMixedFields(model.schema, modelSwagger.properties);
|
|
718
719
|
return modelSwagger.properties as Record<string, OpenApiSchemaProperty>;
|
|
719
720
|
};
|
|
720
721
|
|
package/src/permissions.test.ts
CHANGED
package/src/permissions.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Defaults closed
|
|
2
1
|
import * as Sentry from "@sentry/bun";
|
|
3
2
|
import type express from "express";
|
|
4
3
|
import type {NextFunction} from "express";
|
|
@@ -27,7 +26,6 @@ export const OwnerQueryFilter = (user?: User) => {
|
|
|
27
26
|
if (user) {
|
|
28
27
|
return {ownerId: user?.id};
|
|
29
28
|
}
|
|
30
|
-
// Return a null, so we know to return no results.
|
|
31
29
|
return null;
|
|
32
30
|
};
|
|
33
31
|
|
|
@@ -50,7 +48,7 @@ export const Permissions = {
|
|
|
50
48
|
}
|
|
51
49
|
return method === "list" || method === "read";
|
|
52
50
|
},
|
|
53
|
-
IsOwner: (_method: RESTMethod, user?: User, obj?:
|
|
51
|
+
IsOwner: (_method: RESTMethod, user?: User, obj?: unknown) => {
|
|
54
52
|
// When checking if we can possibly perform the action, return true.
|
|
55
53
|
if (!obj) {
|
|
56
54
|
return true;
|
|
@@ -61,10 +59,12 @@ export const Permissions = {
|
|
|
61
59
|
if (user?.admin) {
|
|
62
60
|
return true;
|
|
63
61
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
62
|
+
const withOwner = obj as {ownerId?: {_id?: unknown} | unknown};
|
|
63
|
+
const ownerObj = withOwner.ownerId as {_id?: unknown} | undefined;
|
|
64
|
+
const ownerId = ownerObj?._id ?? withOwner.ownerId;
|
|
65
|
+
return Boolean(user?.id && ownerId && String(ownerId) === String(user?.id));
|
|
66
66
|
},
|
|
67
|
-
IsOwnerOrReadOnly: (method: RESTMethod, user?: User, obj?:
|
|
67
|
+
IsOwnerOrReadOnly: (method: RESTMethod, user?: User, obj?: unknown) => {
|
|
68
68
|
// When checking if we can possibly perform the action, return true.
|
|
69
69
|
if (!obj) {
|
|
70
70
|
return true;
|
|
@@ -73,37 +73,37 @@ export const Permissions = {
|
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
const withOwner = obj as {ownerId?: unknown};
|
|
77
|
+
if (user?.id && withOwner.ownerId && String(withOwner.ownerId) === String(user?.id)) {
|
|
77
78
|
return true;
|
|
78
79
|
}
|
|
79
80
|
return method === "list" || method === "read";
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
83
|
|
|
83
|
-
export async
|
|
84
|
+
export const checkPermissions = async <T>(
|
|
84
85
|
method: RESTMethod,
|
|
85
86
|
permissions: PermissionMethod<T>[],
|
|
86
87
|
user?: User,
|
|
87
88
|
obj?: T
|
|
88
|
-
): Promise<boolean> {
|
|
89
|
+
): Promise<boolean> => {
|
|
89
90
|
let anyTrue = false;
|
|
90
91
|
for (const perm of permissions) {
|
|
91
|
-
// May or may not be a promise.
|
|
92
92
|
if (!(await perm(method, user, obj))) {
|
|
93
93
|
return false;
|
|
94
94
|
}
|
|
95
95
|
anyTrue = true;
|
|
96
96
|
}
|
|
97
97
|
return anyTrue;
|
|
98
|
-
}
|
|
98
|
+
};
|
|
99
99
|
|
|
100
100
|
// Check the permissions for a given model and method. If the method is a read, update, or delete,
|
|
101
101
|
// finds the relevant object, checks the permissions, and attaches the object to the request as
|
|
102
102
|
// req.obj.
|
|
103
|
-
export
|
|
103
|
+
export const permissionMiddleware = <T>(
|
|
104
104
|
model: Model<T>,
|
|
105
105
|
options: Pick<ModelRouterOptions<T>, "permissions" | "populatePaths">
|
|
106
|
-
) {
|
|
106
|
+
) => {
|
|
107
107
|
return async (req: express.Request, _res: express.Response, next: NextFunction) => {
|
|
108
108
|
if (req.method === "OPTIONS") {
|
|
109
109
|
return next();
|
|
@@ -146,10 +146,14 @@ export function permissionMiddleware<T>(
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const builtQuery = model.findById(req.params.id);
|
|
149
|
-
const populatedQuery = addPopulateToQuery(
|
|
150
|
-
|
|
149
|
+
const populatedQuery = addPopulateToQuery(
|
|
150
|
+
// biome-ignore lint/suspicious/noExplicitAny: Query types vary based on populate paths
|
|
151
|
+
builtQuery as any,
|
|
152
|
+
options.populatePaths
|
|
153
|
+
);
|
|
154
|
+
let data: T | null;
|
|
151
155
|
try {
|
|
152
|
-
data = await populatedQuery.exec();
|
|
156
|
+
data = (await populatedQuery.exec()) as T | null;
|
|
153
157
|
} catch (error: unknown) {
|
|
154
158
|
throw new APIError({
|
|
155
159
|
error: error as Error,
|
|
@@ -174,13 +178,14 @@ export function permissionMiddleware<T>(
|
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
// Document exists but is hidden
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
let reason: {[key: string]: string} | null = null;
|
|
182
|
+
if (hiddenDoc.deleted) {
|
|
183
|
+
reason = {deleted: "true"};
|
|
184
|
+
} else if (hiddenDoc.disabled) {
|
|
185
|
+
reason = {disabled: "true"};
|
|
186
|
+
} else if (hiddenDoc.archived) {
|
|
187
|
+
reason = {archived: "true"};
|
|
188
|
+
}
|
|
184
189
|
|
|
185
190
|
// If no reason found, treat as not found
|
|
186
191
|
if (!reason) {
|
|
@@ -207,7 +212,7 @@ export function permissionMiddleware<T>(
|
|
|
207
212
|
});
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
(req as
|
|
215
|
+
(req as express.Request & {obj?: T | null}).obj = data;
|
|
211
216
|
|
|
212
217
|
return next();
|
|
213
218
|
} catch (error) {
|
|
@@ -215,4 +220,4 @@ export function permissionMiddleware<T>(
|
|
|
215
220
|
return next(error);
|
|
216
221
|
}
|
|
217
222
|
};
|
|
218
|
-
}
|
|
223
|
+
};
|
package/src/plugins.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
1
2
|
import {beforeEach, describe, expect, it, setSystemTime} from "bun:test";
|
|
2
3
|
import type express from "express";
|
|
3
4
|
import {type Document, type Model, model, Schema} from "mongoose";
|
|
@@ -58,7 +59,6 @@ const StuffModel = model<Stuff>("Stuff", stuffSchema) as unknown as StuffModelTy
|
|
|
58
59
|
describe("baseUserPlugin", () => {
|
|
59
60
|
it("adds admin and email fields to the schema", () => {
|
|
60
61
|
const testSchema = new Schema({});
|
|
61
|
-
// biome-ignore lint/suspicious/noExplicitAny: test schema
|
|
62
62
|
baseUserPlugin(testSchema as Schema<any, any, any, any>);
|
|
63
63
|
|
|
64
64
|
const adminPath = testSchema.path("admin");
|
package/src/plugins.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface BaseUser {
|
|
|
16
16
|
email: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
|
|
19
20
|
export const baseUserPlugin = (schema: Schema<any, any, any, any>): void => {
|
|
20
21
|
schema.add({
|
|
21
22
|
admin: {default: false, description: "Whether the user has admin privileges", type: Boolean},
|
|
@@ -29,6 +30,7 @@ export interface IsDeleted {
|
|
|
29
30
|
deleted: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
|
|
32
34
|
export const isDeletedPlugin = (schema: Schema<any, any, any, any>, defaultValue = false): void => {
|
|
33
35
|
schema.add({
|
|
34
36
|
deleted: {
|
|
@@ -40,6 +42,7 @@ export const isDeletedPlugin = (schema: Schema<any, any, any, any>, defaultValue
|
|
|
40
42
|
type: Boolean,
|
|
41
43
|
},
|
|
42
44
|
});
|
|
45
|
+
// biome-ignore lint/suspicious/noExplicitAny: Query<any, any> must be loose to accept arbitrary consumer queries
|
|
43
46
|
const applyDeleteFilter = (q: Query<any, any>): void => {
|
|
44
47
|
const query = q.getQuery();
|
|
45
48
|
if (query && query.deleted === undefined) {
|
|
@@ -55,6 +58,7 @@ export const isDeletedPlugin = (schema: Schema<any, any, any, any>, defaultValue
|
|
|
55
58
|
};
|
|
56
59
|
|
|
57
60
|
export const isDisabledPlugin = (
|
|
61
|
+
// biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
|
|
58
62
|
schema: Schema<any, any, any, any>,
|
|
59
63
|
defaultValue = false
|
|
60
64
|
): void => {
|
|
@@ -73,6 +77,7 @@ export interface CreatedDeleted {
|
|
|
73
77
|
created: {type: Date; required: true};
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
|
|
76
81
|
export const createdUpdatedPlugin = (schema: Schema<any, any, any, any>): void => {
|
|
77
82
|
schema.add({
|
|
78
83
|
updated: {description: "When this document was last updated", index: true, type: Date},
|
|
@@ -111,7 +116,7 @@ export const firebaseJWTPlugin = (schema: Schema): void => {
|
|
|
111
116
|
*/
|
|
112
117
|
export const findOneOrNone = <T>(schema: Schema<T>): void => {
|
|
113
118
|
schema.statics.findOneOrNone = async function (
|
|
114
|
-
query: Record<string,
|
|
119
|
+
query: Record<string, unknown>,
|
|
115
120
|
errorArgs?: Partial<APIErrorConstructor>
|
|
116
121
|
): Promise<(Document & T) | null> {
|
|
117
122
|
const results = await this.find(query);
|
|
@@ -174,7 +179,7 @@ export const findOneOrNoneFor = async <T>(
|
|
|
174
179
|
*/
|
|
175
180
|
export const findExactlyOne = <T>(schema: Schema<T>): void => {
|
|
176
181
|
schema.statics.findExactlyOne = async function (
|
|
177
|
-
query: Record<string,
|
|
182
|
+
query: Record<string, unknown>,
|
|
178
183
|
errorArgs?: Partial<APIErrorConstructor>
|
|
179
184
|
): Promise<Document & T> {
|
|
180
185
|
const results = await this.find(query);
|
|
@@ -204,10 +209,12 @@ export const findExactlyOne = <T>(schema: Schema<T>): void => {
|
|
|
204
209
|
* match the conditions to prevent ambiguous updates.
|
|
205
210
|
* @param schema Mongoose Schema
|
|
206
211
|
*/
|
|
212
|
+
// biome-ignore lint/suspicious/noExplicitAny: Schema generics with unknown collide with mongoose's loose this-binding on schema.statics
|
|
207
213
|
export const upsertPlugin = <T>(schema: Schema<any, any, any, any>): void => {
|
|
208
214
|
schema.statics.upsert = async function (
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
this: mongoose.Model<T>,
|
|
216
|
+
conditions: Record<string, unknown>,
|
|
217
|
+
update: Record<string, unknown>
|
|
211
218
|
): Promise<T> {
|
|
212
219
|
// Try to find the document with the given conditions.
|
|
213
220
|
const docs = await this.find(conditions);
|
|
@@ -223,31 +230,31 @@ export const upsertPlugin = <T>(schema: Schema<any, any, any, any>): void => {
|
|
|
223
230
|
if (doc) {
|
|
224
231
|
// If the document exists, update it with the provided update values.
|
|
225
232
|
Object.assign(doc, update);
|
|
226
|
-
return doc.save();
|
|
233
|
+
return (await doc.save()) as unknown as T;
|
|
227
234
|
}
|
|
228
235
|
// If the document doesn't exist, create a new one with the combined conditions and update
|
|
229
236
|
// values.
|
|
230
237
|
const combinedData = {...conditions, ...update};
|
|
231
238
|
const newDoc = new this(combinedData);
|
|
232
|
-
return newDoc.save();
|
|
239
|
+
return (await newDoc.save()) as unknown as T;
|
|
233
240
|
};
|
|
234
241
|
};
|
|
235
242
|
|
|
236
243
|
/** For models with the upsertPlugin, extend this interface to add the upsert static method. */
|
|
237
244
|
export interface HasUpsert<T> {
|
|
238
|
-
upsert(conditions: Record<string,
|
|
245
|
+
upsert(conditions: Record<string, unknown>, update: Record<string, unknown>): Promise<T>;
|
|
239
246
|
}
|
|
240
247
|
|
|
241
248
|
export interface FindOneOrNonePlugin<T> {
|
|
242
249
|
findOneOrNone(
|
|
243
|
-
query: Record<string,
|
|
250
|
+
query: Record<string, unknown>,
|
|
244
251
|
errorArgs?: Partial<APIErrorConstructor>
|
|
245
252
|
): Promise<(Document & T) | null>;
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
export interface FindExactlyOnePlugin<T> {
|
|
249
256
|
findExactlyOne(
|
|
250
|
-
query: Record<string,
|
|
257
|
+
query: Record<string, unknown>,
|
|
251
258
|
errorArgs?: Partial<APIErrorConstructor>
|
|
252
259
|
): Promise<Document & T>;
|
|
253
260
|
}
|
|
@@ -257,12 +264,12 @@ export class DateOnly extends SchemaType {
|
|
|
257
264
|
super(key, options, "DateOnly");
|
|
258
265
|
}
|
|
259
266
|
|
|
260
|
-
handleSingle(val) {
|
|
267
|
+
handleSingle(val: unknown) {
|
|
261
268
|
return this.cast(val);
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
$conditionalHandlers = {
|
|
265
|
-
// noExplicitAny: $conditionalHandlers is not exposed on SchemaType's prototype in Mongoose's public type definitions
|
|
272
|
+
// biome-ignore lint/suspicious/noExplicitAny: $conditionalHandlers is not exposed on SchemaType's prototype in Mongoose's public type definitions
|
|
266
273
|
...(SchemaType as any).prototype.$conditionalHandlers,
|
|
267
274
|
$gt: this.handleSingle,
|
|
268
275
|
$gte: this.handleSingle,
|
|
@@ -272,9 +279,9 @@ export class DateOnly extends SchemaType {
|
|
|
272
279
|
|
|
273
280
|
// Based on castForQuery in mongoose/lib/schema/date.js
|
|
274
281
|
// When using $gt, $gte, $lt, $lte, etc, we need to cast the value to a Date
|
|
275
|
-
castForQuery($conditional, val, context): Date | undefined {
|
|
282
|
+
castForQuery($conditional: string | undefined, val: unknown, context: unknown): Date | undefined {
|
|
276
283
|
if ($conditional == null) {
|
|
277
|
-
// noExplicitAny: applySetters is an internal Mongoose SchemaType method not in public type definitions
|
|
284
|
+
// biome-ignore lint/suspicious/noExplicitAny: applySetters is an internal Mongoose SchemaType method not in public type definitions
|
|
278
285
|
return (this as any).applySetters(val, context);
|
|
279
286
|
}
|
|
280
287
|
|
|
@@ -334,5 +341,5 @@ export class DateOnly extends SchemaType {
|
|
|
334
341
|
}
|
|
335
342
|
|
|
336
343
|
// Register DateOnly with Mongoose's Schema.Types
|
|
337
|
-
// noExplicitAny: DateOnly is a custom SchemaType not declared in Mongoose's Schema.Types interface
|
|
344
|
+
// biome-ignore lint/suspicious/noExplicitAny: DateOnly is a custom SchemaType not declared in Mongoose's Schema.Types interface
|
|
338
345
|
(mongoose.Schema.Types as any).DateOnly = DateOnly;
|
package/src/populate.test.ts
CHANGED
package/src/populate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import isArray from "lodash/isArray";
|
|
2
|
-
import type {Document} from "mongoose";
|
|
2
|
+
import type {Document, Schema} from "mongoose";
|
|
3
3
|
import m2s from "mongoose-to-swagger";
|
|
4
4
|
|
|
5
5
|
import {APIError} from "./errors";
|
|
@@ -8,7 +8,19 @@ const m2sOptions = {
|
|
|
8
8
|
props: ["readOnly", "required", "enum", "default"],
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface OpenApiSchemaNode {
|
|
12
|
+
description?: string;
|
|
13
|
+
items?: OpenApiSchemaNode;
|
|
14
|
+
properties?: Record<string, OpenApiSchemaNode>;
|
|
15
|
+
type?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SchemaPathInfo {
|
|
19
|
+
instance: string;
|
|
20
|
+
schema?: Schema;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PopulatePath {
|
|
12
24
|
// Mongoose style path population.
|
|
13
25
|
// "ownerId" // populates the User that matches `ownerId`
|
|
14
26
|
// "ownerId.organizationId" Nested. Populates the User that matches `ownerId`, as well as their organization.
|
|
@@ -17,28 +29,25 @@ export type PopulatePath = {
|
|
|
17
29
|
// If not provided and path is provided, will use the path and optionally fields to
|
|
18
30
|
// automatically generate the types. If only generatePathFields is provided, the type will be
|
|
19
31
|
// any.
|
|
20
|
-
openApiComponent?:
|
|
32
|
+
openApiComponent?: string;
|
|
21
33
|
// An array of strings to filter on the populated objects, following Mongoose's select
|
|
22
34
|
// rules. If each field starts a preceding "-", will act as a block list and only remove those
|
|
23
35
|
// fields. If each field does not start with a "-", will act as an allow list and only
|
|
24
36
|
// return those fields.
|
|
25
37
|
fields?: string[];
|
|
26
|
-
}
|
|
38
|
+
}
|
|
27
39
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
// If no keys are provided, it returns the original object.
|
|
31
|
-
// The function recursively traverses the object structure to handle nested properties.
|
|
32
|
-
const filterKeys = (obj: Record<string, any>, keysToKeep?: string[]): Record<string, any> => {
|
|
40
|
+
// Keeps only the specified dot-notation keys from an object.
|
|
41
|
+
const filterKeys = (obj: Record<string, unknown>, keysToKeep?: string[]): Record<string, unknown> => {
|
|
33
42
|
if (!keysToKeep) {
|
|
34
43
|
return obj;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
const result: Record<string,
|
|
46
|
+
const result: Record<string, unknown> = {};
|
|
38
47
|
|
|
39
48
|
const filterNestedKeys = (
|
|
40
|
-
currentObj: Record<string,
|
|
41
|
-
currentResult: Record<string,
|
|
49
|
+
currentObj: Record<string, unknown>,
|
|
50
|
+
currentResult: Record<string, unknown>,
|
|
42
51
|
remainingKeys: string[]
|
|
43
52
|
) => {
|
|
44
53
|
const currentKey = remainingKeys[0];
|
|
@@ -52,7 +61,7 @@ const filterKeys = (obj: Record<string, any>, keysToKeep?: string[]): Record<str
|
|
|
52
61
|
if (!currentResult[firstKey]) {
|
|
53
62
|
currentResult[firstKey] = {};
|
|
54
63
|
}
|
|
55
|
-
filterNestedKeys(currentObj[firstKey], currentResult[firstKey], [
|
|
64
|
+
filterNestedKeys(currentObj[firstKey] as Record<string, unknown>, currentResult[firstKey] as Record<string, unknown>, [
|
|
56
65
|
rest.join("."),
|
|
57
66
|
...remainingKeys.slice(1),
|
|
58
67
|
]);
|
|
@@ -73,7 +82,7 @@ const filterKeys = (obj: Record<string, any>, keysToKeep?: string[]): Record<str
|
|
|
73
82
|
|
|
74
83
|
// Helper function to get the path in the OpenAPI schema, so we can swap out the type for the
|
|
75
84
|
// populated model component or generated type.
|
|
76
|
-
|
|
85
|
+
const getPathInSchema = (schema: OpenApiSchemaNode, path: string): string => {
|
|
77
86
|
const keys = path.split(".");
|
|
78
87
|
let currentSchema = schema;
|
|
79
88
|
let fullPath = "";
|
|
@@ -94,52 +103,50 @@ function getPathInSchema(schema: any, path: string): string {
|
|
|
94
103
|
// If we're at the last key and it's an array, we don't need to add anything
|
|
95
104
|
break;
|
|
96
105
|
} else {
|
|
97
|
-
throw new
|
|
106
|
+
throw new APIError({status: 500, title: `Path ${path} not found in schema at key ${key}`});
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
return fullPath;
|
|
102
|
-
}
|
|
111
|
+
};
|
|
103
112
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
// OpenAPI properties so they use an empty schema (accepts any type) instead
|
|
107
|
-
// of the `{type: "object", properties: {}}` that mongoose-to-swagger emits.
|
|
108
|
-
export const fixMixedFields = (schema: any, properties: Record<string, any>): void => {
|
|
113
|
+
// Corrects Mixed-type fields in OpenAPI properties so they accept any value.
|
|
114
|
+
export const fixMixedFields = (schema: Schema | null, properties: Record<string, OpenApiSchemaNode> | Record<string, unknown> | null): void => {
|
|
109
115
|
if (!properties || !schema) {
|
|
110
116
|
return;
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
const props = properties as Record<string, OpenApiSchemaNode>;
|
|
120
|
+
for (const key of Object.keys(props)) {
|
|
121
|
+
const schemaPath = schema.path(key) as unknown as SchemaPathInfo | undefined;
|
|
115
122
|
if (!schemaPath) {
|
|
116
123
|
continue;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
// Direct Mixed field
|
|
120
127
|
if (schemaPath.instance === "Mixed") {
|
|
121
|
-
|
|
128
|
+
props[key] = {description: props[key]?.description};
|
|
122
129
|
continue;
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
// Array of sub-documents — check each sub-field for Mixed
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
fixMixedFields(schemaPath.schema, properties[key].items.properties);
|
|
133
|
+
if (schemaPath.instance === "Array" && schemaPath.schema) {
|
|
134
|
+
const itemProperties = props[key]?.items?.properties;
|
|
135
|
+
if (itemProperties) {
|
|
136
|
+
fixMixedFields(schemaPath.schema, itemProperties);
|
|
137
|
+
}
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
};
|
|
135
141
|
|
|
136
|
-
export
|
|
142
|
+
export const getOpenApiSpecForModel = (
|
|
143
|
+
// biome-ignore lint/suspicious/noExplicitAny: noExplicitAny: Mongoose Model param uses deep internal APIs (schema.path().options.ref, schema.virtuals, schema.childSchemas, db.model) that are not exposed in public type definitions
|
|
137
144
|
model: any,
|
|
138
145
|
{
|
|
139
146
|
populatePaths,
|
|
140
147
|
extraModelProperties,
|
|
141
148
|
}: {populatePaths?: PopulatePath[]; extraModelProperties?: Record<string, unknown>} = {}
|
|
142
|
-
): {properties: Record<string, unknown>; required: string[]} {
|
|
149
|
+
): {properties: Record<string, unknown>; required: string[]} => {
|
|
143
150
|
const modelSwagger = m2s(model, {
|
|
144
151
|
props: ["required", "enum"],
|
|
145
152
|
});
|
|
@@ -241,19 +248,20 @@ export function getOpenApiSpecForModel(
|
|
|
241
248
|
properties: {...modelSwagger.properties, ...extraModelProperties},
|
|
242
249
|
required: modelSwagger.required ?? [],
|
|
243
250
|
};
|
|
244
|
-
}
|
|
251
|
+
};
|
|
245
252
|
|
|
246
253
|
// Helper function to unpopulate a document that has been populated.
|
|
247
254
|
// This is helpful for supporting backwards compatibility. E.g. you use populatePaths
|
|
248
255
|
// to populate a document but if the version header for the request is below the version
|
|
249
256
|
// that the populatePath was added, we remove the population and just return the _id.
|
|
250
|
-
export
|
|
257
|
+
export const unpopulate = <T>(doc: Document<T>, path: string): Document<T> => {
|
|
251
258
|
if (!path) {
|
|
252
259
|
throw new APIError({status: 500, title: "path is required for unpopulate"});
|
|
253
260
|
}
|
|
254
261
|
const pathParts = path.split(".");
|
|
255
262
|
|
|
256
263
|
// Recursive because we need to support nested paths.
|
|
264
|
+
// biome-ignore lint/suspicious/noExplicitAny: noExplicitAny: recursive document traversal uses bracket-notation indexing on arbitrary nested document shapes that Mongoose Document types do not expose
|
|
257
265
|
const recursiveUnpopulate = (current: any, parts: string[]): any => {
|
|
258
266
|
const part = parts[0];
|
|
259
267
|
|
|
@@ -266,7 +274,7 @@ export function unpopulate<T>(doc: Document<T>, path: string): Document<T> {
|
|
|
266
274
|
// Base case: we've reached the last part of the path
|
|
267
275
|
if (Array.isArray(current[part])) {
|
|
268
276
|
// If the field is an array, recursively unpopulate each element
|
|
269
|
-
current[part] = current[part].map((item
|
|
277
|
+
current[part] = current[part].map((item) => {
|
|
270
278
|
return item?._id ? item._id : item;
|
|
271
279
|
});
|
|
272
280
|
} else if (current[part]?._id) {
|
|
@@ -288,4 +296,4 @@ export function unpopulate<T>(doc: Document<T>, path: string): Document<T> {
|
|
|
288
296
|
};
|
|
289
297
|
|
|
290
298
|
return recursiveUnpopulate(doc, pathParts);
|
|
291
|
-
}
|
|
299
|
+
};
|