@tsed/formio 8.0.1 → 8.0.2
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/package.json +11 -10
- package/src/FormioModule.spec.ts +113 -0
- package/src/FormioModule.ts +130 -0
- package/src/builder/FormioMapper.spec.ts +93 -0
- package/src/builder/FormioMapper.ts +71 -0
- package/src/components/AlterActions.spec.ts +376 -0
- package/src/components/AlterActions.ts +137 -0
- package/src/components/AlterAudit.spec.ts +19 -0
- package/src/components/AlterAudit.ts +12 -0
- package/src/components/AlterHost.spec.ts +20 -0
- package/src/components/AlterHost.ts +11 -0
- package/src/components/AlterLog.spec.ts +19 -0
- package/src/components/AlterLog.ts +12 -0
- package/src/components/AlterSkip.spec.ts +44 -0
- package/src/components/AlterSkip.ts +28 -0
- package/src/components/AlterTemplateExportSteps.spec.ts +99 -0
- package/src/components/AlterTemplateExportSteps.ts +58 -0
- package/src/components/AlterTemplateImportSteps.spec.ts +70 -0
- package/src/components/AlterTemplateImportSteps.ts +50 -0
- package/src/decorators/action.ts +20 -0
- package/src/decorators/actionCtx.spec.ts +25 -0
- package/src/decorators/actionCtx.ts +29 -0
- package/src/decorators/alter.spec.ts +16 -0
- package/src/decorators/alter.ts +19 -0
- package/src/decorators/on.spec.ts +16 -0
- package/src/decorators/on.ts +19 -0
- package/src/decorators/useFormioAuth.spec.ts +15 -0
- package/src/decorators/useFormioAuth.ts +12 -0
- package/src/domain/AlterHook.ts +3 -0
- package/src/domain/Formio.ts +122 -0
- package/src/domain/FormioAction.ts +30 -0
- package/src/domain/FormioActionsIndex.ts +19 -0
- package/src/domain/FormioAuth.ts +83 -0
- package/src/domain/FormioBaseModel.ts +14 -0
- package/src/domain/FormioConfig.ts +63 -0
- package/src/domain/FormioCtxMapper.ts +8 -0
- package/src/domain/FormioDecodedToken.ts +13 -0
- package/src/domain/FormioErrors.ts +53 -0
- package/src/domain/FormioHooks.ts +207 -0
- package/src/domain/FormioJs.ts +18 -0
- package/src/domain/FormioModels.ts +48 -0
- package/src/domain/FormioRouter.ts +10 -0
- package/src/domain/FormioSettings.ts +61 -0
- package/src/domain/FormioTemplate.ts +9 -0
- package/src/domain/FormioTemplateUtil.ts +15 -0
- package/src/domain/FormioUpdate.ts +23 -0
- package/src/domain/FormioUtils.ts +331 -0
- package/src/domain/OnHook.ts +3 -0
- package/src/domain/Resource.ts +21 -0
- package/src/index.ts +46 -0
- package/src/middlewares/FormioAuthMiddleware.spec.ts +61 -0
- package/src/middlewares/FormioAuthMiddleware.ts +33 -0
- package/src/services/FormioAuthService.spec.ts +396 -0
- package/src/services/FormioAuthService.ts +227 -0
- package/src/services/FormioDatabase.spec.ts +326 -0
- package/src/services/FormioDatabase.ts +165 -0
- package/src/services/FormioHooksService.spec.ts +156 -0
- package/src/services/FormioHooksService.ts +91 -0
- package/src/services/FormioInstaller.spec.ts +146 -0
- package/src/services/FormioInstaller.ts +46 -0
- package/src/services/FormioRepository.spec.ts +114 -0
- package/src/services/FormioRepository.ts +49 -0
- package/src/services/FormioService.spec.ts +368 -0
- package/src/services/FormioService.ts +133 -0
- package/src/utils/isMongoId.ts +3 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {Inject, Injectable, InjectorService} from "@tsed/di";
|
|
2
|
+
import {Request} from "express";
|
|
3
|
+
import {promisify} from "util";
|
|
4
|
+
|
|
5
|
+
import {FormioHooks} from "../domain/FormioHooks.js";
|
|
6
|
+
import {FormioService} from "./FormioService.js";
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class FormioHooksService {
|
|
10
|
+
@Inject()
|
|
11
|
+
protected injector: InjectorService;
|
|
12
|
+
|
|
13
|
+
@Inject(FormioService)
|
|
14
|
+
protected formio: FormioService;
|
|
15
|
+
|
|
16
|
+
get settings(): (req: Request, cb: Function) => void {
|
|
17
|
+
return this.formio.hook.settings;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get invoke() {
|
|
21
|
+
return this.formio.hook.invoke;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get alter(): (event: string, ...args: any[]) => any {
|
|
25
|
+
return this.formio.hook.alter;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get alterAsync(): (event: string, ...args: any[]) => Promise<any> {
|
|
29
|
+
return promisify(this.alter);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getHooks(): FormioHooks {
|
|
33
|
+
return {
|
|
34
|
+
alter: this.getHooksProvider("alter"),
|
|
35
|
+
on: this.getHooksProvider("on")
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected getProviders(type: "alter" | "on") {
|
|
40
|
+
return this.injector.getProviders(`formio:${type}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
protected getHooksProvider(type: "alter" | "on") {
|
|
44
|
+
return this.bindHooks(type, this.createHooks(type));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected bindHooks(type: "alter" | "on", hooks: Record<string, Function[]>) {
|
|
48
|
+
return Object.entries(hooks).reduce((newHooks, [key, pool]) => {
|
|
49
|
+
const wrap = (input: any, ...args: any[]): any => {
|
|
50
|
+
let last: any;
|
|
51
|
+
|
|
52
|
+
for (const fn of pool) {
|
|
53
|
+
const result = fn(input, ...args);
|
|
54
|
+
|
|
55
|
+
if (type === "alter") {
|
|
56
|
+
input = result;
|
|
57
|
+
} else {
|
|
58
|
+
last = result;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return type === "alter" ? input : last;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...newHooks,
|
|
67
|
+
[key]: wrap
|
|
68
|
+
};
|
|
69
|
+
}, {});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private createHooks(type: "alter" | "on") {
|
|
73
|
+
return this.getProviders(type).reduce<Record<string, Function[]>>((hooks, provider) => {
|
|
74
|
+
const instance = this.injector.invoke<any>(provider.token);
|
|
75
|
+
const name = provider.store.get(`formio:${type}:name`);
|
|
76
|
+
const pool: Function[] = hooks[name] || [];
|
|
77
|
+
|
|
78
|
+
const hook = (...args: any[]) =>
|
|
79
|
+
instance[type === "alter" ? "transform" : "on"](
|
|
80
|
+
...args.map((input: any) => {
|
|
81
|
+
return input && input.$ctx ? input.$ctx : input;
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
...hooks,
|
|
87
|
+
[name]: ([] as Function[]).concat(pool, hook)
|
|
88
|
+
};
|
|
89
|
+
}, {});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {faker} from "@faker-js/faker";
|
|
2
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
3
|
+
|
|
4
|
+
import {FormioInstaller} from "./FormioInstaller.js";
|
|
5
|
+
import {FormioService} from "./FormioService.js";
|
|
6
|
+
|
|
7
|
+
async function createFormioInstallerFixture(options: any = {}) {
|
|
8
|
+
const {
|
|
9
|
+
count = 1,
|
|
10
|
+
errorCount = null,
|
|
11
|
+
submission = {
|
|
12
|
+
_id: "id",
|
|
13
|
+
data: {}
|
|
14
|
+
},
|
|
15
|
+
errorSubmission = null
|
|
16
|
+
} = options;
|
|
17
|
+
|
|
18
|
+
const collections = {
|
|
19
|
+
estimatedDocumentCount: vi.fn().mockImplementation((cb) => {
|
|
20
|
+
cb(errorCount, count);
|
|
21
|
+
})
|
|
22
|
+
};
|
|
23
|
+
const formioService = {
|
|
24
|
+
db: {
|
|
25
|
+
collection: vi.fn().mockReturnValue(collections)
|
|
26
|
+
},
|
|
27
|
+
formio: {},
|
|
28
|
+
encrypt: vi.fn().mockResolvedValue("hash"),
|
|
29
|
+
resources: {
|
|
30
|
+
submission: {
|
|
31
|
+
model: {
|
|
32
|
+
create: vi.fn().mockImplementation((_, cb) => {
|
|
33
|
+
cb(errorSubmission, submission);
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
importTemplate: vi.fn().mockImplementation((o) => o)
|
|
39
|
+
};
|
|
40
|
+
const service = await PlatformTest.invoke<FormioInstaller>(FormioInstaller, [
|
|
41
|
+
{
|
|
42
|
+
token: FormioService,
|
|
43
|
+
use: formioService
|
|
44
|
+
}
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
return {service, formioService, collections};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("FormioImporter", () => {
|
|
51
|
+
beforeEach(PlatformTest.create);
|
|
52
|
+
afterEach(PlatformTest.reset);
|
|
53
|
+
|
|
54
|
+
describe("createRootUser()", () => {
|
|
55
|
+
it("should create the user root", async () => {
|
|
56
|
+
const {service, formioService} = await createFormioInstallerFixture({count: 1});
|
|
57
|
+
const template = {
|
|
58
|
+
resources: {
|
|
59
|
+
admin: {
|
|
60
|
+
_id: faker.string.uuid()
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
roles: {
|
|
64
|
+
administrator: {
|
|
65
|
+
_id: faker.string.uuid()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const user = {
|
|
71
|
+
email: faker.internet.email(),
|
|
72
|
+
password: faker.internet.password({length: 12})
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
expect(await service.createRootUser(user, template as any)).toEqual({
|
|
76
|
+
_id: "id",
|
|
77
|
+
data: {}
|
|
78
|
+
});
|
|
79
|
+
expect(formioService.resources.submission.model.create).toHaveBeenCalledWith(
|
|
80
|
+
{
|
|
81
|
+
data: {email: user.email, password: "hash"},
|
|
82
|
+
form: template.resources.admin._id,
|
|
83
|
+
roles: [template.roles.administrator._id]
|
|
84
|
+
},
|
|
85
|
+
expect.any(Function)
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
it("should throw error", async () => {
|
|
89
|
+
const {service} = await createFormioInstallerFixture({errorSubmission: new Error("message")});
|
|
90
|
+
const template = {
|
|
91
|
+
resources: {
|
|
92
|
+
admin: {
|
|
93
|
+
_id: faker.string.uuid()
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
roles: {
|
|
97
|
+
administrator: {
|
|
98
|
+
_id: faker.string.uuid()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const user = {
|
|
104
|
+
email: faker.internet.email(),
|
|
105
|
+
password: faker.internet.password({length: 12})
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let actualError: any;
|
|
109
|
+
try {
|
|
110
|
+
await service.createRootUser(user, template as any);
|
|
111
|
+
} catch (er) {
|
|
112
|
+
actualError = er;
|
|
113
|
+
}
|
|
114
|
+
expect(actualError.message).toEqual("message");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("install()", () => {
|
|
118
|
+
it("should install database", async () => {
|
|
119
|
+
const {service, formioService} = await createFormioInstallerFixture({count: 1});
|
|
120
|
+
const template = {
|
|
121
|
+
resources: {
|
|
122
|
+
admin: {
|
|
123
|
+
_id: faker.string.uuid()
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
roles: {
|
|
127
|
+
administrator: {
|
|
128
|
+
_id: faker.string.uuid()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const user = {
|
|
134
|
+
email: faker.internet.email(),
|
|
135
|
+
password: faker.internet.password({length: 12})
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
vi.spyOn(service, "createRootUser");
|
|
139
|
+
|
|
140
|
+
await service.install(template as any, user);
|
|
141
|
+
|
|
142
|
+
expect(formioService.importTemplate).toHaveBeenCalledWith(template);
|
|
143
|
+
expect(service.createRootUser).toHaveBeenCalledWith(user, template);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {Inject, Injectable} from "@tsed/di";
|
|
2
|
+
import {Logger} from "@tsed/logger";
|
|
3
|
+
|
|
4
|
+
import {FormioSubmission} from "../domain/FormioModels.js";
|
|
5
|
+
import {FormioTemplate} from "../domain/FormioTemplate.js";
|
|
6
|
+
import {FormioDatabase} from "./FormioDatabase.js";
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class FormioInstaller extends FormioDatabase {
|
|
10
|
+
@Inject()
|
|
11
|
+
protected logger: Logger;
|
|
12
|
+
|
|
13
|
+
async install(template: FormioTemplate, root: any) {
|
|
14
|
+
this.logger.info("Install formio template...");
|
|
15
|
+
template = await this.formio.importTemplate(template);
|
|
16
|
+
|
|
17
|
+
if (root) {
|
|
18
|
+
this.logger.info("Create root user...");
|
|
19
|
+
await this.createRootUser(root, template);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async createRootUser<User = unknown>(user: {email: string; password: string}, template: FormioTemplate): Promise<FormioSubmission<User>> {
|
|
24
|
+
const hash = await this.formio.encrypt(user.password);
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
this.formio.resources.submission.model.create(
|
|
28
|
+
{
|
|
29
|
+
form: template.resources.admin._id,
|
|
30
|
+
data: {
|
|
31
|
+
email: user.email,
|
|
32
|
+
password: hash
|
|
33
|
+
},
|
|
34
|
+
roles: [template.roles?.administrator._id].filter(Boolean)
|
|
35
|
+
},
|
|
36
|
+
(err: unknown, item: any) => {
|
|
37
|
+
if (err) {
|
|
38
|
+
return reject(err);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
resolve(item);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {Injectable} from "@tsed/di";
|
|
2
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
3
|
+
|
|
4
|
+
import {FormioDatabase} from "./FormioDatabase.js";
|
|
5
|
+
import {FormioRepository} from "./FormioRepository.js";
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
class PackagesRepository extends FormioRepository {
|
|
9
|
+
formName = "package";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("FormioRepository", () => {
|
|
13
|
+
beforeEach(() => PlatformTest.create());
|
|
14
|
+
afterEach(() => PlatformTest.reset());
|
|
15
|
+
|
|
16
|
+
describe("saveSubmission()", () => {
|
|
17
|
+
it("should create submission", async () => {
|
|
18
|
+
const database = {
|
|
19
|
+
formModel: {
|
|
20
|
+
findOne: vi.fn().mockResolvedValue({
|
|
21
|
+
_id: "id"
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
saveSubmission: vi.fn().mockImplementation((o) => o)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const service = await PlatformTest.invoke<PackagesRepository>(PackagesRepository, [
|
|
28
|
+
{
|
|
29
|
+
token: FormioDatabase,
|
|
30
|
+
use: database
|
|
31
|
+
}
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const result = await service.saveSubmission({
|
|
35
|
+
data: {
|
|
36
|
+
label: "label"
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
data: {
|
|
42
|
+
label: "label"
|
|
43
|
+
},
|
|
44
|
+
form: "id"
|
|
45
|
+
});
|
|
46
|
+
expect(database.saveSubmission).toHaveBeenCalledWith({
|
|
47
|
+
data: {
|
|
48
|
+
label: "label"
|
|
49
|
+
},
|
|
50
|
+
form: "id"
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("getSubmissions()", () => {
|
|
55
|
+
it("should get all saved submissions", async () => {
|
|
56
|
+
const database = {
|
|
57
|
+
formModel: {
|
|
58
|
+
findOne: vi.fn().mockResolvedValue({
|
|
59
|
+
_id: "id"
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
getSubmissions: vi.fn().mockResolvedValue([])
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const service = await PlatformTest.invoke<PackagesRepository>(PackagesRepository, [
|
|
66
|
+
{
|
|
67
|
+
token: FormioDatabase,
|
|
68
|
+
use: database
|
|
69
|
+
}
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const submissions = await service.getSubmissions();
|
|
73
|
+
|
|
74
|
+
expect(submissions).toEqual([]);
|
|
75
|
+
expect(database.formModel.findOne).toHaveBeenCalledWith({
|
|
76
|
+
name: {
|
|
77
|
+
$eq: "package"
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
expect(database.getSubmissions).toHaveBeenCalledWith({form: "id"});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("findOneSubmission()", () => {
|
|
84
|
+
it("should find on submission", async () => {
|
|
85
|
+
const database = {
|
|
86
|
+
formModel: {
|
|
87
|
+
findOne: vi.fn().mockResolvedValue({
|
|
88
|
+
_id: "id"
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
submissionModel: {
|
|
92
|
+
findOne: vi.fn().mockResolvedValue({_id: "id"})
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const service = await PlatformTest.invoke<PackagesRepository>(PackagesRepository, [
|
|
97
|
+
{
|
|
98
|
+
token: FormioDatabase,
|
|
99
|
+
use: database
|
|
100
|
+
}
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const submission = await service.findOneSubmission({});
|
|
104
|
+
|
|
105
|
+
expect(submission).toEqual({_id: "id"});
|
|
106
|
+
expect(database.formModel.findOne).toHaveBeenCalledWith({
|
|
107
|
+
name: {
|
|
108
|
+
$eq: "package"
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
expect(database.submissionModel.findOne).toHaveBeenCalledWith({deleted: null, form: "id"});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {Inject} from "@tsed/di";
|
|
2
|
+
import {MongooseDocument, MongooseModel} from "@tsed/mongoose";
|
|
3
|
+
import type {FilterQuery} from "mongoose";
|
|
4
|
+
|
|
5
|
+
import {FormioSubmission} from "../domain/FormioModels.js";
|
|
6
|
+
import {FormioDatabase} from "./FormioDatabase.js";
|
|
7
|
+
|
|
8
|
+
export abstract class FormioRepository<SubmissionData = any> {
|
|
9
|
+
@Inject()
|
|
10
|
+
protected formioDatabase: FormioDatabase;
|
|
11
|
+
|
|
12
|
+
protected abstract formName: string;
|
|
13
|
+
|
|
14
|
+
private formId: string;
|
|
15
|
+
|
|
16
|
+
async getFormId() {
|
|
17
|
+
if (!this.formId) {
|
|
18
|
+
const form = await this.formioDatabase.formModel.findOne({name: {$eq: this.formName}});
|
|
19
|
+
|
|
20
|
+
if (form) {
|
|
21
|
+
this.formId = form._id;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return this.formId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async saveSubmission(submission: Omit<Partial<FormioSubmission<SubmissionData>>, "form"> & {form?: any}) {
|
|
29
|
+
return this.formioDatabase.saveSubmission<SubmissionData>({
|
|
30
|
+
...submission,
|
|
31
|
+
form: submission.form || (await this.getFormId())
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getSubmissions(query: FilterQuery<MongooseModel<FormioSubmission<SubmissionData>>> = {}) {
|
|
36
|
+
return this.formioDatabase.getSubmissions<SubmissionData>({
|
|
37
|
+
...query,
|
|
38
|
+
form: await this.getFormId()
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async findOneSubmission(query: any): Promise<MongooseDocument<FormioSubmission<SubmissionData>> | undefined> {
|
|
43
|
+
return this.formioDatabase.submissionModel.findOne({
|
|
44
|
+
form: await this.getFormId(),
|
|
45
|
+
deleted: null,
|
|
46
|
+
...query
|
|
47
|
+
}) as any;
|
|
48
|
+
}
|
|
49
|
+
}
|