@internetderdinge/api 1.229.32 → 1.229.39

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 (54) hide show
  1. package/dist/src/accounts/accounts.route.js +18 -5
  2. package/dist/src/admin/adminSearch.controller.js +0 -19
  3. package/dist/src/admin/adminSearch.route.js +5 -5
  4. package/dist/src/devices/devices.controller.js +4 -6
  5. package/dist/src/devices/devices.route.js +14 -14
  6. package/dist/src/devices/devices.validation.js +15 -54
  7. package/dist/src/email/email.service.js +6 -6
  8. package/dist/src/index.js +5 -1
  9. package/dist/src/iotdevice/iotdevice.route.js +3 -1
  10. package/dist/src/iotdevice/iotdevice.service.js +5 -5
  11. package/dist/src/iotdevice/iotdevice.validation.js +12 -4
  12. package/dist/src/middlewares/validateAdminOrSupport.js +20 -0
  13. package/dist/src/organizations/organizations.controller.js +17 -6
  14. package/dist/src/organizations/organizations.route.js +2 -1
  15. package/dist/src/users/users.model.js +4 -2
  16. package/dist/src/users/users.route.js +2 -2
  17. package/dist/src/users/users.service.js +26 -10
  18. package/dist/src/users/users.validation.js +60 -61
  19. package/dist/src/utils/buildRouterAndDocs.js +7 -4
  20. package/dist/src/utils/zValidations.js +7 -0
  21. package/package.json +5 -2
  22. package/scripts/release-and-sync-paperless.mjs +21 -2
  23. package/scripts/release-version.mjs +21 -2
  24. package/src/accounts/accounts.route.ts +21 -5
  25. package/src/admin/adminSearch.controller.ts +0 -35
  26. package/src/admin/adminSearch.route.ts +8 -6
  27. package/src/admin/adminSearch.service.ts +6 -10
  28. package/src/devices/devices.controller.ts +4 -12
  29. package/src/devices/devices.route.ts +14 -15
  30. package/src/devices/devices.validation.ts +20 -54
  31. package/src/email/email.service.ts +15 -7
  32. package/src/index.ts +5 -1
  33. package/src/iotdevice/iotdevice.route.ts +3 -1
  34. package/src/iotdevice/iotdevice.service.ts +8 -7
  35. package/src/iotdevice/iotdevice.validation.ts +12 -4
  36. package/src/middlewares/validateAdminOrSupport.ts +34 -0
  37. package/src/organizations/organizations.controller.ts +38 -7
  38. package/src/organizations/organizations.route.ts +3 -1
  39. package/src/users/users.model.ts +7 -3
  40. package/src/users/users.route.ts +3 -2
  41. package/src/users/users.service.ts +50 -14
  42. package/src/users/users.validation.ts +62 -60
  43. package/src/utils/buildRouterAndDocs.ts +14 -5
  44. package/src/utils/zValidations.ts +8 -0
  45. package/dist/src/pdf/pdf.controller.js +0 -24
  46. package/dist/src/pdf/pdf.route.js +0 -22
  47. package/dist/src/pdf/pdf.schemas.js +0 -6
  48. package/dist/src/pdf/pdf.service.js +0 -64
  49. package/dist/src/pdf/pdf.validation.js +0 -27
  50. package/src/pdf/pdf.controller.ts +0 -35
  51. package/src/pdf/pdf.route.ts +0 -28
  52. package/src/pdf/pdf.schemas.ts +0 -7
  53. package/src/pdf/pdf.service.ts +0 -103
  54. package/src/pdf/pdf.validation.ts +0 -30
@@ -14,37 +14,39 @@ import {
14
14
 
15
15
  extendZodWithOpenApi(z);
16
16
 
17
- export const createUserSchema = {
18
- body: z.object({
19
- meta: z
20
- .object({})
21
- .passthrough()
22
- .optional()
23
- .openapi({
24
- example: { key: "value" },
25
- description: "Additional metadata for the user",
26
- }),
27
- organization: zObjectId.openapi({
28
- description: "Organization ObjectId",
29
- }),
30
- email: z.string().email().optional().nullable().openapi({
31
- example: "user@example.com",
32
- description: "User email address",
33
- }),
34
- timezone: z.string().optional().openapi({
35
- example: "Europe/Berlin",
36
- description: "IANA timezone string",
37
- }),
38
- role: z.enum(["user", "admin", "patient", "onlyself"]).optional().openapi({
39
- description: "Role assigned to the user",
17
+ export const userAppsSchema = z
18
+ .record(z.string(), z.unknown())
19
+ .optional()
20
+ .openapi({ description: "Application-specific user fields" });
21
+
22
+ export const createUserBodyShape = {
23
+ meta: z
24
+ .object({})
25
+ .passthrough()
26
+ .optional()
27
+ .openapi({
28
+ example: { key: "value" },
29
+ description: "Additional metadata for the user",
40
30
  }),
41
- category: z
42
- .enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
43
- .optional()
44
- .openapi({
45
- description: "Category of the user",
46
- }),
31
+ apps: userAppsSchema,
32
+ organization: zObjectId.openapi({
33
+ description: "Organization ObjectId",
34
+ }),
35
+ email: z.string().email().optional().nullable().openapi({
36
+ example: "user@example.com",
37
+ description: "User email address",
38
+ }),
39
+ timezone: z.string().optional().openapi({
40
+ example: "Europe/Berlin",
41
+ description: "IANA timezone string",
47
42
  }),
43
+ role: z.enum(["user", "admin", "patient", "onlyself"]).optional().openapi({
44
+ description: "Role assigned to the user",
45
+ }),
46
+ };
47
+
48
+ export const createUserSchema = {
49
+ body: z.object(createUserBodyShape),
48
50
  };
49
51
 
50
52
  export const createCurrentUserSchema = createUserSchema;
@@ -69,39 +71,39 @@ export const getCurrentUserSchema = {
69
71
  }),
70
72
  };
71
73
 
74
+ export const updateUserBodyShape = {
75
+ name: z.string().optional().openapi({ description: "User full name" }),
76
+ timezone: z.string().optional().openapi({ description: "IANA timezone" }),
77
+ avatar: z.string().optional().openapi({ description: "Avatar URL" }),
78
+ meta: z
79
+ .object({})
80
+ .passthrough()
81
+ .optional()
82
+ .openapi({ description: "Additional metadata" }),
83
+ apps: userAppsSchema,
84
+ email: z
85
+ .string()
86
+ .email()
87
+ .nullable()
88
+ .optional()
89
+ .openapi({ description: "User email address" }),
90
+ role: z
91
+ .enum(["user", "admin", "patient", "onlyself"])
92
+ .optional()
93
+ .openapi({ description: "User role" }),
94
+ inviteCode: z
95
+ .string()
96
+ .nullable()
97
+ .optional()
98
+ .openapi({ description: "Invite code" }),
99
+ organization: zObjectId
100
+ .optional()
101
+ .openapi({ description: "Organization ObjectId" }),
102
+ };
103
+
72
104
  export const updateUserSchema = {
73
105
  ...zUpdate("userId"),
74
- body: zPatchBody({
75
- name: z.string().optional().openapi({ description: "User full name" }),
76
- timezone: z.string().optional().openapi({ description: "IANA timezone" }),
77
- avatar: z.string().optional().openapi({ description: "Avatar URL" }),
78
- meta: z
79
- .object({})
80
- .optional()
81
- .openapi({ description: "Additional metadata" }),
82
- category: z
83
- .enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
84
- .optional()
85
- .openapi({ description: "User category" }),
86
- email: z
87
- .string()
88
- .email()
89
- .nullable()
90
- .optional()
91
- .openapi({ description: "User email address" }),
92
- role: z
93
- .enum(["user", "admin", "patient", "onlyself"])
94
- .optional()
95
- .openapi({ description: "User role" }),
96
- inviteCode: z
97
- .string()
98
- .nullable()
99
- .optional()
100
- .openapi({ description: "Invite code" }),
101
- organization: zObjectId
102
- .optional()
103
- .openapi({ description: "Organization ObjectId" }),
104
- }),
106
+ body: zPatchBody(updateUserBodyShape),
105
107
  };
106
108
 
107
109
  export const deleteUserSchema = zDelete("userId");
@@ -30,16 +30,25 @@ export type RouteSpec = {
30
30
  summary: string;
31
31
  description?: string;
32
32
  privateDocs?: boolean;
33
- memoOnly?: boolean;
33
+ };
34
+
35
+ export type BuildRouterAndDocsOptions = {
36
+ routeSpecs?: (specs: RouteSpec[]) => RouteSpec[];
37
+ includeInDocs?: (spec: RouteSpec) => boolean;
34
38
  };
35
39
 
36
40
  export default function buildAiRouterAndDocs(
37
41
  router: Router,
38
- routeSpecs: any,
42
+ routeSpecs: RouteSpec[],
39
43
  basePath = "/",
40
44
  tags: string[] = [],
45
+ options: BuildRouterAndDocsOptions = {},
41
46
  ) {
42
- routeSpecs.forEach((spec) => {
47
+ const effectiveRouteSpecs = options.routeSpecs
48
+ ? options.routeSpecs(routeSpecs)
49
+ : routeSpecs;
50
+
51
+ effectiveRouteSpecs.forEach((spec) => {
43
52
  const validate = spec.validate || [];
44
53
  const routeMiddleware =
45
54
  spec.validateWithRequestSchema ||
@@ -68,7 +77,7 @@ export default function buildAiRouterAndDocs(
68
77
  spec.responseSchema &&
69
78
  !hasRoleValidation(spec.validateWithRequestSchema || validate) &&
70
79
  spec.privateDocs !== true &&
71
- spec.memoOnly !== true
80
+ (options.includeInDocs ? options.includeInDocs(spec) : true)
72
81
  ) {
73
82
  // collect all middleware fn names (falls back to '<anonymous>' if unnamed)
74
83
  const middlewareNames = (spec.validateWithRequestSchema || validate).map(
@@ -96,7 +105,7 @@ export default function buildAiRouterAndDocs(
96
105
  // (optionally) expose them as a custom extension instead:
97
106
  "x-middlewares": middlewareNames,
98
107
 
99
- security: [{ [bearerAuth.name]: [] }, { [xApiKey.name]: [] }],
108
+ security: [{ [xApiKey.name]: [] }, { [bearerAuth.name]: [] }],
100
109
  responses: {
101
110
  200: {
102
111
  description: "Object with user data.",
@@ -158,3 +158,11 @@ export const zDelete = (id: string) => ({
158
158
  export const zObjectId = zObjectIdFor();
159
159
 
160
160
  export const zDate = () => z.string().pipe(z.coerce.date());
161
+
162
+ export const zTypeFilter = z
163
+ .string()
164
+ .openapi({
165
+ description: "Event type filter. Common values include activate and state; any other string is accepted.",
166
+ example: "activate",
167
+ })
168
+ .optional();
@@ -1,24 +0,0 @@
1
- import httpStatus from "http-status";
2
- import catchAsync from "../utils/catchAsync.js";
3
- import pdfService from "./pdf.service.js";
4
- import ApiError from "../utils/ApiError.js";
5
- export const generatePdfFromUrl = catchAsync(async (req, res) => {
6
- const fileName = "memo-print";
7
- const authHeader = req.headers["authorization"];
8
- if (!authHeader) {
9
- throw new ApiError(httpStatus.UNAUTHORIZED, "Missing Authorization header");
10
- }
11
- const token = authHeader.split(" ")[1];
12
- if (!token) {
13
- throw new ApiError(httpStatus.UNAUTHORIZED, "Missing bearer token");
14
- }
15
- const urlPath = typeof req.query.urlPath === "string" ? req.query.urlPath : undefined;
16
- if (!urlPath) {
17
- throw new ApiError(httpStatus.BAD_REQUEST, "Missing urlPath query parameter");
18
- }
19
- const result = await pdfService.generatePdfFromUrl({ urlPath, token });
20
- res.status(httpStatus.CREATED).send({ signed: result });
21
- });
22
- export default {
23
- generatePdfFromUrl,
24
- };
@@ -1,22 +0,0 @@
1
- import { Router } from "express";
2
- import buildRouterAndDocs from "../utils/buildRouterAndDocs.js";
3
- import { generatePdfSchema } from "./pdf.validation.js";
4
- import { pdfResponseSchema } from "./pdf.schemas.js";
5
- import { generatePdfFromUrl } from "./pdf.controller.js";
6
- import auth from "../middlewares/auth.js";
7
- export const pdfRouteSpecs = [
8
- {
9
- method: "get",
10
- path: "/",
11
- validate: [auth("manageUsers")],
12
- requestSchema: generatePdfSchema,
13
- responseSchema: pdfResponseSchema,
14
- handler: generatePdfFromUrl,
15
- summary: "Generate a PDF from a provided URL",
16
- description: "This endpoint allows users to generate a PDF document from a specified URL.",
17
- memoOnly: true,
18
- },
19
- ];
20
- const router = Router();
21
- buildRouterAndDocs(router, pdfRouteSpecs, "/pdf", ["PDF"]);
22
- export default router;
@@ -1,6 +0,0 @@
1
- import { z } from 'zod';
2
- export const pdfResponseSchema = z.object({
3
- url: z.string().url(),
4
- filename: z.string(),
5
- size: z.number(),
6
- });
@@ -1,64 +0,0 @@
1
- import puppeteer from "puppeteer";
2
- import { v4 as uuidv4 } from "uuid";
3
- import { GetObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
4
- import { getSignedUrl as getS3SignedUrl } from "@aws-sdk/s3-request-presigner";
5
- import path from "path";
6
- const s3 = new S3Client({
7
- region: process.env.AWS_REGION,
8
- credentials: process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
9
- ? {
10
- accessKeyId: process.env.AWS_ACCESS_KEY_ID,
11
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
12
- }
13
- : undefined,
14
- });
15
- // Function to upload a file to S3
16
- const uploadBuffer = async (buffer, fileName) => {
17
- await s3.send(new PutObjectCommand({
18
- Bucket: process.env.AWS_S3_BUCKET_NAME,
19
- Key: fileName,
20
- Body: buffer,
21
- ContentType: "application/pdf",
22
- }));
23
- };
24
- // Generate a signed URL
25
- const generateSignedUrl = async (fileName) => {
26
- return getS3SignedUrl(s3, new GetObjectCommand({
27
- Bucket: process.env.AWS_S3_BUCKET_NAME,
28
- Key: fileName,
29
- }), { expiresIn: 60 * 60 });
30
- };
31
- const generatePdfFromUrl = async ({ urlPath, token, }) => {
32
- const domain = process.env.FRONTEND_URL;
33
- const browser = await puppeteer.launch({
34
- defaultViewport: {
35
- width: 1300,
36
- height: 1200,
37
- deviceScaleFactor: 1,
38
- },
39
- executablePath: process.env.CHROME_BIN,
40
- args: ["--no-sandbox"],
41
- });
42
- const page = await browser.newPage();
43
- await page.goto(domain);
44
- // Set the token in local storage
45
- await page.evaluate((token) => {
46
- localStorage.setItem("print-token", token);
47
- }, token);
48
- await page.goto(domain + urlPath, { waitUntil: "networkidle2" });
49
- await page.waitForSelector(".pdf-render-complete");
50
- const pdf = await page.pdf({ format: "A4", printBackground: true });
51
- await page.evaluate(() => {
52
- localStorage.setItem("print-token", "");
53
- });
54
- await browser.close();
55
- const fileName = `download-${uuidv4()}.pdf`;
56
- await uploadBuffer(Buffer.from(pdf), fileName);
57
- console.log(`File uploaded successfully. File Name: ${fileName}`);
58
- const signedUrl = await generateSignedUrl(path.basename(fileName));
59
- console.log(`Signed URL: ${signedUrl}`);
60
- return signedUrl;
61
- };
62
- export default {
63
- generatePdfFromUrl,
64
- };
@@ -1,27 +0,0 @@
1
- import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
2
- import { z } from 'zod';
3
- extendZodWithOpenApi(z);
4
- export const generatePdfSchema = {
5
- query: z.object({
6
- urlPath: z
7
- .string()
8
- /* .url()
9
- .refine(
10
- (value) =>
11
- value.startsWith('https://memo.wirewire.de') ||
12
- value.startsWith('https://web.wirewire.de') ||
13
- (process.env.NODE_ENV !== 'production' && value.startsWith('http://localhost:3200')),
14
- {
15
- message:
16
- process.env.NODE_ENV !== 'production'
17
- ? 'urlPath must start with https://memo.wirewire.de, https://web.wirewire.de, or http://localhost:3200'
18
- : 'urlPath must start with https://memo.wirewire.de or https://web.wirewire.de',
19
- }
20
- ) */
21
- .openapi({
22
- example: 'https://memo.wirewire.de/example.pdf',
23
- description: 'URL path to the PDF generation endpoint. Allowed domains: https://memo.wirewire.de, https://web.wirewire.de' +
24
- (process.env.NODE_ENV !== 'production' ? ', or http://localhost:3200 (dev)' : ''),
25
- }),
26
- }),
27
- };
@@ -1,35 +0,0 @@
1
- import httpStatus from "http-status";
2
- import catchAsync from "../utils/catchAsync.js";
3
- import pdfService from "./pdf.service.js";
4
- import ApiError from "../utils/ApiError.js";
5
-
6
- export const generatePdfFromUrl = catchAsync(async (req, res) => {
7
- const fileName = "memo-print";
8
-
9
- const authHeader = req.headers["authorization"];
10
- if (!authHeader) {
11
- throw new ApiError(httpStatus.UNAUTHORIZED, "Missing Authorization header");
12
- }
13
-
14
- const token = authHeader.split(" ")[1];
15
- if (!token) {
16
- throw new ApiError(httpStatus.UNAUTHORIZED, "Missing bearer token");
17
- }
18
-
19
- const urlPath =
20
- typeof req.query.urlPath === "string" ? req.query.urlPath : undefined;
21
- if (!urlPath) {
22
- throw new ApiError(
23
- httpStatus.BAD_REQUEST,
24
- "Missing urlPath query parameter",
25
- );
26
- }
27
-
28
- const result = await pdfService.generatePdfFromUrl({ urlPath, token });
29
-
30
- res.status(httpStatus.CREATED).send({ signed: result });
31
- });
32
-
33
- export default {
34
- generatePdfFromUrl,
35
- };
@@ -1,28 +0,0 @@
1
- import { Router } from "express";
2
- import buildRouterAndDocs from "../utils/buildRouterAndDocs.js";
3
- import { generatePdfSchema } from "./pdf.validation.js";
4
- import { pdfResponseSchema } from "./pdf.schemas.js";
5
- import { generatePdfFromUrl } from "./pdf.controller.js";
6
- import type { RouteSpec } from "../types/routeSpec";
7
- import auth from "../middlewares/auth.js";
8
-
9
- export const pdfRouteSpecs: RouteSpec[] = [
10
- {
11
- method: "get",
12
- path: "/",
13
- validate: [auth("manageUsers")],
14
- requestSchema: generatePdfSchema,
15
- responseSchema: pdfResponseSchema,
16
- handler: generatePdfFromUrl,
17
- summary: "Generate a PDF from a provided URL",
18
- description:
19
- "This endpoint allows users to generate a PDF document from a specified URL.",
20
- memoOnly: true,
21
- },
22
- ];
23
-
24
- const router: Router = Router();
25
-
26
- buildRouterAndDocs(router, pdfRouteSpecs, "/pdf", ["PDF"]);
27
-
28
- export default router;
@@ -1,7 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- export const pdfResponseSchema = z.object({
4
- url: z.string().url(),
5
- filename: z.string(),
6
- size: z.number(),
7
- });
@@ -1,103 +0,0 @@
1
- import puppeteer from "puppeteer";
2
- import { v4 as uuidv4 } from "uuid";
3
- import {
4
- GetObjectCommand,
5
- PutObjectCommand,
6
- S3Client,
7
- } from "@aws-sdk/client-s3";
8
- import { getSignedUrl as getS3SignedUrl } from "@aws-sdk/s3-request-presigner";
9
- import path from "path";
10
-
11
- const s3 = new S3Client({
12
- region: process.env.AWS_REGION,
13
- credentials:
14
- process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
15
- ? {
16
- accessKeyId: process.env.AWS_ACCESS_KEY_ID,
17
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
18
- }
19
- : undefined,
20
- });
21
-
22
- // Function to upload a file to S3
23
- const uploadBuffer = async (
24
- buffer: Buffer,
25
- fileName: string,
26
- ): Promise<void> => {
27
- await s3.send(
28
- new PutObjectCommand({
29
- Bucket: process.env.AWS_S3_BUCKET_NAME!,
30
- Key: fileName,
31
- Body: buffer,
32
- ContentType: "application/pdf",
33
- }),
34
- );
35
- };
36
-
37
- // Generate a signed URL
38
- const generateSignedUrl = async (fileName: string): Promise<string> => {
39
- return getS3SignedUrl(
40
- s3,
41
- new GetObjectCommand({
42
- Bucket: process.env.AWS_S3_BUCKET_NAME!,
43
- Key: fileName,
44
- }),
45
- { expiresIn: 60 * 60 },
46
- );
47
- };
48
-
49
- interface GeneratePdfOptions {
50
- urlPath: string;
51
- token: string;
52
- }
53
-
54
- const generatePdfFromUrl = async ({
55
- urlPath,
56
- token,
57
- }: GeneratePdfOptions): Promise<string> => {
58
- const domain = process.env.FRONTEND_URL!;
59
- const browser = await puppeteer.launch({
60
- defaultViewport: {
61
- width: 1300,
62
- height: 1200,
63
- deviceScaleFactor: 1,
64
- },
65
- executablePath: process.env.CHROME_BIN,
66
- args: ["--no-sandbox"],
67
- });
68
-
69
- const page = await browser.newPage();
70
-
71
- await page.goto(domain);
72
-
73
- // Set the token in local storage
74
- await page.evaluate((token) => {
75
- localStorage.setItem("print-token", token);
76
- }, token);
77
-
78
- await page.goto(domain + urlPath, { waitUntil: "networkidle2" });
79
-
80
- await page.waitForSelector(".pdf-render-complete");
81
-
82
- const pdf = await page.pdf({ format: "A4", printBackground: true });
83
-
84
- await page.evaluate(() => {
85
- localStorage.setItem("print-token", "");
86
- });
87
-
88
- await browser.close();
89
-
90
- const fileName = `download-${uuidv4()}.pdf`;
91
- await uploadBuffer(Buffer.from(pdf), fileName);
92
-
93
- console.log(`File uploaded successfully. File Name: ${fileName}`);
94
-
95
- const signedUrl = await generateSignedUrl(path.basename(fileName));
96
- console.log(`Signed URL: ${signedUrl}`);
97
-
98
- return signedUrl;
99
- };
100
-
101
- export default {
102
- generatePdfFromUrl,
103
- };
@@ -1,30 +0,0 @@
1
- import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
2
- import { z } from 'zod';
3
-
4
- extendZodWithOpenApi(z);
5
-
6
- export const generatePdfSchema = {
7
- query: z.object({
8
- urlPath: z
9
- .string()
10
- /* .url()
11
- .refine(
12
- (value) =>
13
- value.startsWith('https://memo.wirewire.de') ||
14
- value.startsWith('https://web.wirewire.de') ||
15
- (process.env.NODE_ENV !== 'production' && value.startsWith('http://localhost:3200')),
16
- {
17
- message:
18
- process.env.NODE_ENV !== 'production'
19
- ? 'urlPath must start with https://memo.wirewire.de, https://web.wirewire.de, or http://localhost:3200'
20
- : 'urlPath must start with https://memo.wirewire.de or https://web.wirewire.de',
21
- }
22
- ) */
23
- .openapi({
24
- example: 'https://memo.wirewire.de/example.pdf',
25
- description:
26
- 'URL path to the PDF generation endpoint. Allowed domains: https://memo.wirewire.de, https://web.wirewire.de' +
27
- (process.env.NODE_ENV !== 'production' ? ', or http://localhost:3200 (dev)' : ''),
28
- }),
29
- }),
30
- };