@terreno/api 0.7.2 → 0.8.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 (50) hide show
  1. package/dist/__tests__/{versionCheck.test.js → versionCheckPlugin.test.js} +2 -2
  2. package/dist/api.d.ts +4 -2
  3. package/dist/api.js +7 -2
  4. package/dist/consentApp.d.ts +33 -0
  5. package/dist/consentApp.js +484 -0
  6. package/dist/consentApp.test.d.ts +1 -0
  7. package/dist/consentApp.test.js +1132 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.js +6 -0
  10. package/dist/models/consentForm.d.ts +2 -0
  11. package/dist/models/consentForm.js +115 -0
  12. package/dist/models/consentResponse.d.ts +2 -0
  13. package/dist/models/consentResponse.js +73 -0
  14. package/dist/models/versionConfig.d.ts +1 -1
  15. package/dist/openApiValidator.js +2 -0
  16. package/dist/populate.d.ts +1 -0
  17. package/dist/populate.js +53 -13
  18. package/dist/syncConsents.d.ts +67 -0
  19. package/dist/syncConsents.js +334 -0
  20. package/dist/syncConsents.test.d.ts +1 -0
  21. package/dist/syncConsents.test.js +249 -0
  22. package/dist/terrenoApp.js +6 -5
  23. package/dist/terrenoPlugin.d.ts +1 -1
  24. package/dist/types/consentForm.d.ts +32 -0
  25. package/dist/types/consentForm.js +2 -0
  26. package/dist/types/consentResponse.d.ts +23 -0
  27. package/dist/types/consentResponse.js +2 -0
  28. package/dist/vendor/wesleytodd-openapi/lib/generate-doc.js +1 -1
  29. package/dist/versionCheckPlugin.d.ts +2 -0
  30. package/dist/versionCheckPlugin.js +3 -6
  31. package/package.json +1 -1
  32. package/src/__tests__/{versionCheck.test.ts → versionCheckPlugin.test.ts} +2 -2
  33. package/src/api.ts +11 -4
  34. package/src/consentApp.test.ts +749 -0
  35. package/src/consentApp.ts +463 -0
  36. package/src/index.ts +6 -0
  37. package/src/models/consentForm.ts +123 -0
  38. package/src/models/consentResponse.ts +78 -0
  39. package/src/models/versionConfig.ts +1 -1
  40. package/src/openApiValidator.ts +2 -0
  41. package/src/populate.ts +33 -0
  42. package/src/syncConsents.test.ts +124 -0
  43. package/src/syncConsents.ts +263 -0
  44. package/src/terrenoApp.ts +6 -6
  45. package/src/terrenoPlugin.ts +1 -1
  46. package/src/types/consentForm.ts +41 -0
  47. package/src/types/consentResponse.ts +34 -0
  48. package/src/vendor/wesleytodd-openapi/lib/generate-doc.js +1 -1
  49. package/src/versionCheckPlugin.ts +5 -6
  50. /package/dist/__tests__/{versionCheck.test.d.ts → versionCheckPlugin.test.d.ts} +0 -0
@@ -43,6 +43,7 @@ import m2s from "mongoose-to-swagger";
43
43
  import {APIError} from "./errors";
44
44
  import {logger} from "./logger";
45
45
  import type {OpenApiSchema, OpenApiSchemaProperty} from "./openApiBuilder";
46
+ import {fixMixedFields} from "./populate";
46
47
 
47
48
  /**
48
49
  * Global configuration for OpenAPI validation.
@@ -713,6 +714,7 @@ const m2sOptions = {
713
714
  */
714
715
  export function getSchemaFromModel<T>(model: Model<T>): Record<string, OpenApiSchemaProperty> {
715
716
  const modelSwagger = m2s(model, m2sOptions);
717
+ fixMixedFields((model as any).schema, modelSwagger.properties);
716
718
  return modelSwagger.properties as Record<string, OpenApiSchemaProperty>;
717
719
  }
718
720
 
package/src/populate.ts CHANGED
@@ -102,6 +102,37 @@ function getPathInSchema(schema: any, path: string): string {
102
102
  }
103
103
 
104
104
  // Replaces populated properties with the populated schema.
105
+ // Recursively walks a Mongoose schema and fixes any Mixed fields in the
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 => {
109
+ if (!properties || !schema) {
110
+ return;
111
+ }
112
+
113
+ for (const key of Object.keys(properties)) {
114
+ const schemaPath = schema.path(key);
115
+ if (!schemaPath) {
116
+ continue;
117
+ }
118
+
119
+ // Direct Mixed field
120
+ if (schemaPath.instance === "Mixed") {
121
+ properties[key] = {description: properties[key]?.description};
122
+ continue;
123
+ }
124
+
125
+ // Array of sub-documents — check each sub-field for Mixed
126
+ if (
127
+ schemaPath.instance === "Array" &&
128
+ schemaPath.schema &&
129
+ properties[key]?.items?.properties
130
+ ) {
131
+ fixMixedFields(schemaPath.schema, properties[key].items.properties);
132
+ }
133
+ }
134
+ };
135
+
105
136
  export function getOpenApiSpecForModel(
106
137
  model: any,
107
138
  {
@@ -113,6 +144,8 @@ export function getOpenApiSpecForModel(
113
144
  props: ["required", "enum"],
114
145
  });
115
146
 
147
+ fixMixedFields(model.schema, modelSwagger.properties);
148
+
116
149
  if (populatePaths && isArray(populatePaths)) {
117
150
  for (const populatePath of populatePaths) {
118
151
  // Get the referenced populate model from the model schema
@@ -0,0 +1,124 @@
1
+ import {afterEach, beforeEach, describe, expect, it} from "bun:test";
2
+ import {ConsentForm} from "./models/consentForm";
3
+ import type {ConsentFormDefinition} from "./syncConsents";
4
+ import {syncConsents} from "./syncConsents";
5
+ import {setupDb} from "./tests";
6
+
7
+ const baseDef: ConsentFormDefinition = {
8
+ content: {en: "# Terms\nPlease agree."},
9
+ order: 1,
10
+ required: true,
11
+ title: "Terms of Service",
12
+ type: "terms",
13
+ };
14
+
15
+ describe("syncConsents", () => {
16
+ beforeEach(async () => {
17
+ await setupDb();
18
+ await ConsentForm.deleteMany({});
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await ConsentForm.deleteMany({});
23
+ });
24
+
25
+ it("creates a new consent form when none exists", async () => {
26
+ const result = await syncConsents({terms: baseDef});
27
+
28
+ expect(result.created).toEqual(["terms"]);
29
+ expect(result.updated).toHaveLength(0);
30
+ expect(result.unchanged).toHaveLength(0);
31
+
32
+ const forms = await ConsentForm.find({slug: "terms"});
33
+ expect(forms).toHaveLength(1);
34
+ expect(forms[0].active).toBe(true);
35
+ expect(forms[0].version).toBe(1);
36
+ expect(forms[0].title).toBe("Terms of Service");
37
+ });
38
+
39
+ it("leaves unchanged forms alone", async () => {
40
+ await syncConsents({terms: baseDef});
41
+ const result = await syncConsents({terms: baseDef});
42
+
43
+ expect(result.unchanged).toEqual(["terms"]);
44
+ expect(result.created).toHaveLength(0);
45
+ expect(result.updated).toHaveLength(0);
46
+
47
+ const forms = await ConsentForm.find({slug: "terms"});
48
+ expect(forms).toHaveLength(1);
49
+ expect(forms[0].version).toBe(1);
50
+ });
51
+
52
+ it("publishes a new version when content changes", async () => {
53
+ await syncConsents({terms: baseDef});
54
+
55
+ const updated = {...baseDef, content: {en: "# Updated Terms\nNew content."}};
56
+ const result = await syncConsents({terms: updated});
57
+
58
+ expect(result.updated).toEqual(["terms"]);
59
+
60
+ const forms = await ConsentForm.find({slug: "terms"}).sort({version: 1});
61
+ expect(forms).toHaveLength(2);
62
+ expect(forms[0].active).toBe(false);
63
+ expect(forms[0].version).toBe(1);
64
+ expect(forms[1].active).toBe(true);
65
+ expect(forms[1].version).toBe(2);
66
+ });
67
+
68
+ it("publishes a new version when title changes", async () => {
69
+ await syncConsents({terms: baseDef});
70
+
71
+ const updated = {...baseDef, title: "Updated Terms"};
72
+ const result = await syncConsents({terms: updated});
73
+
74
+ expect(result.updated).toEqual(["terms"]);
75
+
76
+ const active = await ConsentForm.findOne({active: true, slug: "terms"});
77
+ expect(active?.version).toBe(2);
78
+ expect(active?.title).toBe("Updated Terms");
79
+ });
80
+
81
+ it("deactivates removed forms when deactivateRemoved is true", async () => {
82
+ await syncConsents({privacy: {...baseDef, title: "Privacy", type: "privacy"}, terms: baseDef});
83
+
84
+ const result = await syncConsents({terms: baseDef}, {deactivateRemoved: true});
85
+
86
+ expect(result.deactivated).toEqual(["privacy"]);
87
+ expect(result.unchanged).toEqual(["terms"]);
88
+
89
+ const privacy = await ConsentForm.findOne({slug: "privacy"});
90
+ expect(privacy?.active).toBe(false);
91
+ });
92
+
93
+ it("does not deactivate removed forms by default", async () => {
94
+ await syncConsents({privacy: {...baseDef, title: "Privacy", type: "privacy"}, terms: baseDef});
95
+
96
+ const result = await syncConsents({terms: baseDef});
97
+
98
+ expect(result.deactivated).toHaveLength(0);
99
+ const privacy = await ConsentForm.findOne({slug: "privacy"});
100
+ expect(privacy?.active).toBe(true);
101
+ });
102
+
103
+ it("does not write to the database in dry run mode", async () => {
104
+ const result = await syncConsents({terms: baseDef}, {dryRun: true});
105
+
106
+ expect(result.created).toEqual(["terms"]);
107
+ const forms = await ConsentForm.find({slug: "terms"});
108
+ expect(forms).toHaveLength(0);
109
+ });
110
+
111
+ it("handles multiple forms in a single sync", async () => {
112
+ const result = await syncConsents({
113
+ privacy: {...baseDef, order: 2, title: "Privacy Policy", type: "privacy"},
114
+ terms: baseDef,
115
+ });
116
+
117
+ expect(result.created.sort()).toEqual(["privacy", "terms"]);
118
+
119
+ const forms = await ConsentForm.find({}).sort({order: 1});
120
+ expect(forms).toHaveLength(2);
121
+ expect(forms[0].slug).toBe("terms");
122
+ expect(forms[1].slug).toBe("privacy");
123
+ });
124
+ });
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Sync consent form definitions from code to the database.
3
+ *
4
+ * Compares the provided definitions (keyed by slug) against what's in the database
5
+ * and creates, updates, or deactivates forms to match. When content changes, a new
6
+ * version is published so users are prompted to re-consent.
7
+ */
8
+
9
+ import {logger} from "./logger";
10
+ import {ConsentForm} from "./models/consentForm";
11
+ import type {ConsentFormType} from "./types/consentForm";
12
+
13
+ export interface ConsentFormDefinition {
14
+ title: string;
15
+ type: ConsentFormType;
16
+ content: Record<string, string>;
17
+ order?: number;
18
+ required?: boolean;
19
+ requireScrollToBottom?: boolean;
20
+ captureSignature?: boolean;
21
+ agreeButtonText?: string;
22
+ allowDecline?: boolean;
23
+ declineButtonText?: string;
24
+ defaultLocale?: string;
25
+ checkboxes?: Array<{
26
+ label: string;
27
+ required?: boolean;
28
+ confirmationPrompt?: string;
29
+ }>;
30
+ }
31
+
32
+ export interface SyncConsentsOptions {
33
+ /** Deactivate database forms whose slugs are not in the definitions. Default: false */
34
+ deactivateRemoved?: boolean;
35
+ /** If true, log what would change without writing to the database. Default: false */
36
+ dryRun?: boolean;
37
+ }
38
+
39
+ export interface SyncConsentsResult {
40
+ created: string[];
41
+ updated: string[];
42
+ deactivated: string[];
43
+ unchanged: string[];
44
+ }
45
+
46
+ const contentEqual = (a: Map<string, string>, b: Record<string, string>): boolean => {
47
+ const aKeys = [...a.keys()].sort();
48
+ const bKeys = Object.keys(b).sort();
49
+ if (aKeys.length !== bKeys.length) {
50
+ return false;
51
+ }
52
+ return aKeys.every((key, i) => key === bKeys[i] && a.get(key) === b[key]);
53
+ };
54
+
55
+ const formFieldsMatch = (
56
+ existing: {
57
+ title: string;
58
+ type: string;
59
+ order: number;
60
+ required: boolean;
61
+ requireScrollToBottom: boolean;
62
+ captureSignature: boolean;
63
+ agreeButtonText: string;
64
+ allowDecline: boolean;
65
+ declineButtonText: string;
66
+ defaultLocale: string;
67
+ checkboxes: Array<{label: string; required: boolean; confirmationPrompt?: string}>;
68
+ content: Map<string, string>;
69
+ },
70
+ def: ConsentFormDefinition
71
+ ): boolean => {
72
+ if (existing.title !== def.title) {
73
+ return false;
74
+ }
75
+ if (existing.type !== def.type) {
76
+ return false;
77
+ }
78
+ if (existing.order !== (def.order ?? 0)) {
79
+ return false;
80
+ }
81
+ if (existing.required !== (def.required ?? true)) {
82
+ return false;
83
+ }
84
+ if (existing.requireScrollToBottom !== (def.requireScrollToBottom ?? false)) {
85
+ return false;
86
+ }
87
+ if (existing.captureSignature !== (def.captureSignature ?? false)) {
88
+ return false;
89
+ }
90
+ if (existing.agreeButtonText !== (def.agreeButtonText ?? "I Agree")) {
91
+ return false;
92
+ }
93
+ if (existing.allowDecline !== (def.allowDecline ?? false)) {
94
+ return false;
95
+ }
96
+ if (existing.declineButtonText !== (def.declineButtonText ?? "Decline")) {
97
+ return false;
98
+ }
99
+ if (existing.defaultLocale !== (def.defaultLocale ?? "en")) {
100
+ return false;
101
+ }
102
+ if (!contentEqual(existing.content, def.content)) {
103
+ return false;
104
+ }
105
+
106
+ const existingCheckboxes = existing.checkboxes ?? [];
107
+ const defCheckboxes = def.checkboxes ?? [];
108
+ if (existingCheckboxes.length !== defCheckboxes.length) {
109
+ return false;
110
+ }
111
+ for (let i = 0; i < existingCheckboxes.length; i++) {
112
+ const ec = existingCheckboxes[i];
113
+ const dc = defCheckboxes[i];
114
+ if (ec.label !== dc.label || ec.required !== (dc.required ?? false)) {
115
+ return false;
116
+ }
117
+ if ((ec.confirmationPrompt ?? undefined) !== (dc.confirmationPrompt ?? undefined)) {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ return true;
123
+ };
124
+
125
+ /**
126
+ * Sync consent form definitions to the database.
127
+ *
128
+ * @param definitions - Map of slug to consent form definition
129
+ * @param options - Sync options
130
+ * @returns Summary of what was created, updated, deactivated, or unchanged
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * import {syncConsents} from "@terreno/api";
135
+ *
136
+ * await syncConsents({
137
+ * "terms-of-service": {
138
+ * title: "Terms of Service",
139
+ * type: "terms",
140
+ * content: {"en": "# Terms\n...", "es": "# Términos\n..."},
141
+ * required: true,
142
+ * order: 1,
143
+ * },
144
+ * "privacy-policy": {
145
+ * title: "Privacy Policy",
146
+ * type: "privacy",
147
+ * content: {"en": "# Privacy\n..."},
148
+ * order: 2,
149
+ * },
150
+ * });
151
+ * ```
152
+ */
153
+ export const syncConsents = async (
154
+ definitions: Record<string, ConsentFormDefinition>,
155
+ options: SyncConsentsOptions = {}
156
+ ): Promise<SyncConsentsResult> => {
157
+ const {deactivateRemoved = false, dryRun = false} = options;
158
+
159
+ const result: SyncConsentsResult = {
160
+ created: [],
161
+ deactivated: [],
162
+ unchanged: [],
163
+ updated: [],
164
+ };
165
+
166
+ const slugs = Object.keys(definitions);
167
+
168
+ // Fetch the current active form for each slug
169
+ const activeForms = await ConsentForm.find({active: true});
170
+ const activeBySlug = new Map(activeForms.map((f) => [f.slug, f]));
171
+
172
+ for (const slug of slugs) {
173
+ const def = definitions[slug];
174
+ const existing = activeBySlug.get(slug);
175
+
176
+ if (!existing) {
177
+ // No active form for this slug — create version 1
178
+ logger.info(`syncConsents: creating "${slug}"`, {dryRun});
179
+ if (!dryRun) {
180
+ await ConsentForm.create({
181
+ active: true,
182
+ agreeButtonText: def.agreeButtonText,
183
+ allowDecline: def.allowDecline,
184
+ captureSignature: def.captureSignature,
185
+ checkboxes: def.checkboxes,
186
+ content: new Map(Object.entries(def.content)),
187
+ declineButtonText: def.declineButtonText,
188
+ defaultLocale: def.defaultLocale,
189
+ order: def.order ?? 0,
190
+ required: def.required ?? true,
191
+ requireScrollToBottom: def.requireScrollToBottom,
192
+ slug,
193
+ title: def.title,
194
+ type: def.type,
195
+ version: 1,
196
+ });
197
+ }
198
+ result.created.push(slug);
199
+ continue;
200
+ }
201
+
202
+ if (formFieldsMatch(existing, def)) {
203
+ result.unchanged.push(slug);
204
+ continue;
205
+ }
206
+
207
+ // Content or config changed — publish a new version
208
+ const newVersion = existing.version + 1;
209
+ logger.info(`syncConsents: updating "${slug}" v${existing.version} -> v${newVersion}`, {
210
+ dryRun,
211
+ });
212
+ if (!dryRun) {
213
+ await ConsentForm.create({
214
+ active: true,
215
+ agreeButtonText: def.agreeButtonText,
216
+ allowDecline: def.allowDecline,
217
+ captureSignature: def.captureSignature,
218
+ checkboxes: def.checkboxes,
219
+ content: new Map(Object.entries(def.content)),
220
+ declineButtonText: def.declineButtonText,
221
+ defaultLocale: def.defaultLocale,
222
+ order: def.order ?? 0,
223
+ required: def.required ?? true,
224
+ requireScrollToBottom: def.requireScrollToBottom,
225
+ slug,
226
+ title: def.title,
227
+ type: def.type,
228
+ version: newVersion,
229
+ });
230
+ await ConsentForm.updateMany(
231
+ {_id: {$ne: undefined}, slug, version: {$lt: newVersion}},
232
+ {active: false}
233
+ );
234
+ }
235
+ result.updated.push(slug);
236
+ }
237
+
238
+ // Deactivate forms that are no longer in definitions
239
+ if (deactivateRemoved) {
240
+ for (const [slug, form] of activeBySlug) {
241
+ if (!definitions[slug]) {
242
+ logger.info(`syncConsents: deactivating "${slug}"`, {dryRun});
243
+ if (!dryRun) {
244
+ await ConsentForm.updateMany({slug}, {active: false});
245
+ }
246
+ result.deactivated.push(slug);
247
+ }
248
+ }
249
+ }
250
+
251
+ const summary = [
252
+ result.created.length > 0 ? `created: ${result.created.join(", ")}` : null,
253
+ result.updated.length > 0 ? `updated: ${result.updated.join(", ")}` : null,
254
+ result.deactivated.length > 0 ? `deactivated: ${result.deactivated.join(", ")}` : null,
255
+ result.unchanged.length > 0 ? `unchanged: ${result.unchanged.join(", ")}` : null,
256
+ ]
257
+ .filter(Boolean)
258
+ .join(" | ");
259
+
260
+ logger.info(`syncConsents: ${dryRun ? "[DRY RUN] " : ""}${summary || "nothing to do"}`);
261
+
262
+ return result;
263
+ };
package/src/terrenoApp.ts CHANGED
@@ -301,8 +301,6 @@ export class TerrenoApp {
301
301
  app.use("/swagger", oapi.swaggerui());
302
302
  }
303
303
 
304
- addMeRoutes(app, options.userModel as any, options.authOptions);
305
-
306
304
  // GitHub OAuth
307
305
  if (options.githubAuth) {
308
306
  setupGitHubAuth(app, options.userModel as any, options.githubAuth);
@@ -317,14 +315,16 @@ export class TerrenoApp {
317
315
  // Mount registered model routers and plugins
318
316
  for (const registration of this.registrations) {
319
317
  if (this.isModelRouterRegistration(registration)) {
320
- app.use(registration.path, registration.router);
318
+ const router = registration._buildWithOpenApi(oapi);
319
+ app.use(registration.path, router);
321
320
  } else {
322
- registration.register(app);
321
+ registration.register(app, oapi);
323
322
  }
324
323
  }
325
324
 
326
- // Inject openApi into model router options for registered routers
327
- // The openApi middleware handles this via the oapi instance already mounted on the app
325
+ // /auth/me must be registered after plugins so that session middleware
326
+ // (e.g. Better Auth) has a chance to populate req.user first.
327
+ addMeRoutes(app, options.userModel as any, options.authOptions);
328
328
 
329
329
  Sentry.setupExpressErrorHandler(app);
330
330
 
@@ -35,5 +35,5 @@ export interface TerrenoPlugin {
35
35
  *
36
36
  * @param app - The Express application instance to register with
37
37
  */
38
- register(app: express.Application): void;
38
+ register(app: express.Application, openApi?: unknown): void;
39
39
  }
@@ -0,0 +1,41 @@
1
+ import type mongoose from "mongoose";
2
+ import type {FindExactlyOnePlugin, FindOneOrNonePlugin} from "../plugins";
3
+
4
+ export interface ConsentFormCheckbox {
5
+ label: string;
6
+ required: boolean;
7
+ confirmationPrompt?: string;
8
+ }
9
+
10
+ export type ConsentFormType = "agreement" | "privacy" | "hipaa" | "research" | "terms" | "custom";
11
+
12
+ // biome-ignore lint/complexity/noBannedTypes: No methods.
13
+ export type ConsentFormMethods = {};
14
+
15
+ export type ConsentFormStatics = FindExactlyOnePlugin<ConsentFormDocument> &
16
+ FindOneOrNonePlugin<ConsentFormDocument>;
17
+
18
+ export type ConsentFormModel = mongoose.Model<ConsentFormDocument, object, ConsentFormMethods> &
19
+ ConsentFormStatics;
20
+
21
+ export interface ConsentFormDocument extends mongoose.Document {
22
+ _id: mongoose.Types.ObjectId;
23
+ title: string;
24
+ slug: string;
25
+ version: number;
26
+ order: number;
27
+ type: ConsentFormType;
28
+ content: Map<string, string>;
29
+ defaultLocale: string;
30
+ active: boolean;
31
+ captureSignature: boolean;
32
+ requireScrollToBottom: boolean;
33
+ checkboxes: ConsentFormCheckbox[];
34
+ agreeButtonText: string;
35
+ allowDecline: boolean;
36
+ declineButtonText: string;
37
+ required: boolean;
38
+ created: Date;
39
+ updated: Date;
40
+ deleted: boolean;
41
+ }
@@ -0,0 +1,34 @@
1
+ import type mongoose from "mongoose";
2
+ import type {FindExactlyOnePlugin, FindOneOrNonePlugin} from "../plugins";
3
+
4
+ // biome-ignore lint/complexity/noBannedTypes: No methods.
5
+ export type ConsentResponseMethods = {};
6
+
7
+ export type ConsentResponseStatics = FindExactlyOnePlugin<ConsentResponseDocument> &
8
+ FindOneOrNonePlugin<ConsentResponseDocument>;
9
+
10
+ export type ConsentResponseModel = mongoose.Model<
11
+ ConsentResponseDocument,
12
+ object,
13
+ ConsentResponseMethods
14
+ > &
15
+ ConsentResponseStatics;
16
+
17
+ export interface ConsentResponseDocument extends mongoose.Document {
18
+ _id: mongoose.Types.ObjectId;
19
+ userId: mongoose.Types.ObjectId;
20
+ consentFormId: mongoose.Types.ObjectId;
21
+ agreed: boolean;
22
+ agreedAt: Date;
23
+ checkboxValues?: Map<string, boolean>;
24
+ locale: string;
25
+ signature?: string;
26
+ signedAt?: Date;
27
+ ipAddress?: string;
28
+ userAgent?: string;
29
+ contentSnapshot?: string;
30
+ formVersionSnapshot?: number;
31
+ created: Date;
32
+ updated: Date;
33
+ deleted: boolean;
34
+ }
@@ -114,7 +114,7 @@ function processComplexMatch (thing, keys) {
114
114
  // (i.e. /:id, /:name, etc...) with the name(s) of those parameter(s)
115
115
  // This could have been accomplished with replaceAll for Node version 15 and above
116
116
  // no-useless-escape is disabled since we need three backslashes
117
- .replace(/\(\?\:\(\[\^\\\/\]\+\?\)\)/g, () => `{${keys[i++].name}}`) // eslint-disable-line no-useless-escape
117
+ .replace(/\(\?\:\(\[\^\\\/\]\+\?\)\)/g, () => `{${keys[i++].name}}`)
118
118
  .replace(/\\(.)/g, '$1')
119
119
  // The replace below removes the regex used at the start of the string and
120
120
  // the regex used to match the query parameters
@@ -8,8 +8,10 @@ export type VersionCheckStatus = "ok" | "warning" | "required";
8
8
 
9
9
  export interface VersionCheckResponse {
10
10
  message?: string;
11
+ requiredVersion?: number;
11
12
  status: VersionCheckStatus;
12
13
  updateUrl?: string;
14
+ warningVersion?: number;
13
15
  }
14
16
 
15
17
  const DEFAULT_WARNING_MESSAGE =
@@ -57,21 +59,18 @@ export class VersionCheckPlugin implements TerrenoPlugin {
57
59
  : (config.mobileWarningVersion ?? 0);
58
60
 
59
61
  const response: VersionCheckResponse = {
62
+ requiredVersion: requiredVersion > 0 ? requiredVersion : undefined,
60
63
  status: "ok",
64
+ updateUrl: config.updateUrl || undefined,
65
+ warningVersion: warningVersion > 0 ? warningVersion : undefined,
61
66
  };
62
67
 
63
68
  if (requiredVersion > 0 && version < requiredVersion) {
64
69
  response.status = "required";
65
70
  response.message = config.requiredMessage ?? DEFAULT_REQUIRED_MESSAGE;
66
- if (config.updateUrl) {
67
- response.updateUrl = config.updateUrl;
68
- }
69
71
  } else if (warningVersion > 0 && version < warningVersion) {
70
72
  response.status = "warning";
71
73
  response.message = config.warningMessage ?? DEFAULT_WARNING_MESSAGE;
72
- if (config.updateUrl) {
73
- response.updateUrl = config.updateUrl;
74
- }
75
74
  }
76
75
 
77
76
  return res.json(response);