@sentzunhat/zacatl 0.0.12 → 0.0.14
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 +3 -1
- package/src/index.ts +1 -0
- package/src/logs.ts +9 -15
- package/src/micro-service/architecture/application/application.ts +3 -3
- package/src/micro-service/architecture/application/entry-points/rest/common/handler.ts +3 -2
- package/src/micro-service/architecture/application/entry-points/rest/route-handlers/route-handler.ts +3 -2
- package/src/micro-service/architecture/architecture.ts +3 -1
- package/src/micro-service/architecture/domain/domain.ts +2 -2
- package/src/micro-service/architecture/infrastructure/infrastructure.ts +2 -2
- package/src/micro-service/architecture/infrastructure/repositories/abstract.ts +59 -145
- package/src/micro-service/architecture/infrastructure/repositories/mongoose.ts +124 -0
- package/src/micro-service/architecture/infrastructure/repositories/sequelize.ts +70 -0
- package/src/micro-service/architecture/infrastructure/repositories/types.ts +69 -0
- package/src/micro-service/architecture/platform/service/service.ts +36 -5
- package/test/unit/micro-service/architecture/infrastructure/repositories/abstract.test.ts +13 -7
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@sentzunhat/zacatl",
|
|
3
3
|
"main": "src/index.ts",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.14",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/sentzunhat/zacatl.git"
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"@types/config": "^3.3.5",
|
|
66
66
|
"@types/i18n": "^0.13.12",
|
|
67
67
|
"@types/node": "^22.7.4",
|
|
68
|
+
"@types/sequelize": "^4.28.20",
|
|
68
69
|
"@types/uuid": "^10.0.0",
|
|
69
70
|
"@typescript-eslint/parser": "^8.29.0",
|
|
70
71
|
"@vitest/coverage-istanbul": "^3.1.1",
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
"i18n": "^0.15.1",
|
|
85
86
|
"mongodb-memory-server": "^10.1.4",
|
|
86
87
|
"mongoose": "^8.15.0",
|
|
88
|
+
"sequelize": "^6.37.5",
|
|
87
89
|
"pino": "^9.7.0",
|
|
88
90
|
"pino-pretty": "^13.0.0",
|
|
89
91
|
"reflect-metadata": "^0.2.2",
|
package/src/index.ts
CHANGED
package/src/logs.ts
CHANGED
|
@@ -31,50 +31,44 @@ const defaultLogger: pino.BaseLogger = pino({
|
|
|
31
31
|
|
|
32
32
|
export type LoggerInput =
|
|
33
33
|
| {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
data?: unknown;
|
|
35
|
+
details?: unknown;
|
|
36
36
|
}
|
|
37
37
|
| undefined;
|
|
38
38
|
|
|
39
39
|
const logger = {
|
|
40
40
|
log: (message: string, input?: LoggerInput): void => {
|
|
41
41
|
console.log(message, {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
data: input?.data,
|
|
43
|
+
details: input?.details,
|
|
44
44
|
});
|
|
45
45
|
},
|
|
46
46
|
|
|
47
47
|
info: (message: string, input?: LoggerInput): void => {
|
|
48
|
-
defaultLogger.info(
|
|
49
|
-
{ logData: input?.logData, metadata: input?.metadata },
|
|
50
|
-
message
|
|
51
|
-
);
|
|
48
|
+
defaultLogger.info({ data: input?.data, details: input?.details }, message);
|
|
52
49
|
},
|
|
53
50
|
|
|
54
51
|
trace: (message: string, input?: LoggerInput): void => {
|
|
55
52
|
defaultLogger.trace(
|
|
56
|
-
{
|
|
53
|
+
{ data: input?.data, details: input?.details },
|
|
57
54
|
message
|
|
58
55
|
);
|
|
59
56
|
},
|
|
60
57
|
|
|
61
58
|
warn: (message: string, input?: LoggerInput): void => {
|
|
62
|
-
defaultLogger.warn(
|
|
63
|
-
{ logData: input?.logData, metadata: input?.metadata },
|
|
64
|
-
message
|
|
65
|
-
);
|
|
59
|
+
defaultLogger.warn({ data: input?.data, details: input?.details }, message);
|
|
66
60
|
},
|
|
67
61
|
|
|
68
62
|
error: (message: string, input?: LoggerInput): void => {
|
|
69
63
|
defaultLogger.error(
|
|
70
|
-
{
|
|
64
|
+
{ data: input?.data, details: input?.details },
|
|
71
65
|
message
|
|
72
66
|
);
|
|
73
67
|
},
|
|
74
68
|
|
|
75
69
|
fatal: (message: string, input?: LoggerInput): void => {
|
|
76
70
|
defaultLogger.fatal(
|
|
77
|
-
{
|
|
71
|
+
{ data: input?.data, details: input?.details },
|
|
78
72
|
message
|
|
79
73
|
);
|
|
80
74
|
},
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import i18n from "i18n";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
|
-
import { AbstractArchitecture } from "../architecture";
|
|
4
|
+
import { AbstractArchitecture, Constructor } from "../architecture";
|
|
5
5
|
import { HookHandler, RouteHandler } from "./entry-points/rest";
|
|
6
6
|
|
|
7
|
-
export type ApplicationHookHandlers = Array<
|
|
8
|
-
export type ApplicationRouteHandlers = Array<
|
|
7
|
+
export type ApplicationHookHandlers = Array<Constructor<HookHandler>>;
|
|
8
|
+
export type ApplicationRouteHandlers = Array<Constructor<RouteHandler>>;
|
|
9
9
|
|
|
10
10
|
export type ApplicationEntryPoints = {
|
|
11
11
|
rest: {
|
|
@@ -6,8 +6,9 @@ export type Handler<
|
|
|
6
6
|
TBody = void,
|
|
7
7
|
TQuerystring = void,
|
|
8
8
|
TParams = void,
|
|
9
|
-
THeaders = void
|
|
9
|
+
THeaders = void,
|
|
10
|
+
TReply = unknown
|
|
10
11
|
> = (
|
|
11
12
|
request: Request<TBody, TQuerystring, TParams, THeaders>,
|
|
12
13
|
reply: FastifyReply
|
|
13
|
-
) => Promise<
|
|
14
|
+
) => Promise<TReply>;
|
package/src/micro-service/architecture/application/entry-points/rest/route-handlers/route-handler.ts
CHANGED
|
@@ -6,10 +6,11 @@ export type RouteHandler<
|
|
|
6
6
|
TBody = void,
|
|
7
7
|
TQuerystring = void,
|
|
8
8
|
TParams = void,
|
|
9
|
-
THeaders = void
|
|
9
|
+
THeaders = void,
|
|
10
|
+
TReply = unknown
|
|
10
11
|
> = {
|
|
11
12
|
url: string;
|
|
12
13
|
method: HTTPMethods;
|
|
13
14
|
schema: FastifySchema;
|
|
14
|
-
execute: Handler<TBody, TQuerystring, TParams, THeaders>;
|
|
15
|
+
execute: Handler<TBody, TQuerystring, TParams, THeaders, TReply>;
|
|
15
16
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { container } from "tsyringe";
|
|
2
2
|
|
|
3
|
+
export type Constructor<T = unknown> = new (...args: unknown[]) => T;
|
|
4
|
+
|
|
3
5
|
type Architecture = {
|
|
4
6
|
start: () => void;
|
|
5
7
|
};
|
|
@@ -9,7 +11,7 @@ export abstract class AbstractArchitecture implements Architecture {
|
|
|
9
11
|
* A generic helper method to register an array of handler classes.
|
|
10
12
|
* @param handlers - An array of class constructors that implement the handler functionality.
|
|
11
13
|
*/
|
|
12
|
-
protected registerDependencies<T>(dependencies: Array<
|
|
14
|
+
protected registerDependencies<T>(dependencies: Array<Constructor<T>>): void {
|
|
13
15
|
for (const dependency of dependencies) {
|
|
14
16
|
const instance = new dependency();
|
|
15
17
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AbstractArchitecture } from "../architecture";
|
|
1
|
+
import { AbstractArchitecture, Constructor } from "../architecture";
|
|
2
2
|
|
|
3
3
|
export type ConfigDomain = {
|
|
4
|
-
providers: Array<
|
|
4
|
+
providers: Array<Constructor>;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
export class Domain extends AbstractArchitecture {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AbstractArchitecture } from "../architecture";
|
|
1
|
+
import { AbstractArchitecture, Constructor } from "../architecture";
|
|
2
2
|
|
|
3
3
|
export type ConfigInfrastructure = {
|
|
4
|
-
repositories: Array<
|
|
4
|
+
repositories: Array<Constructor>;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
export class Infrastructure extends AbstractArchitecture {
|
|
@@ -1,176 +1,90 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export type BaseRepositoryConfig<D> = {
|
|
16
|
-
name?: string;
|
|
17
|
-
schema: Schema<D>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type WithMongooseMeta<T> = Default__v<Require_id<T>>;
|
|
21
|
-
|
|
22
|
-
export type MongooseDocument<Db> = Document<
|
|
23
|
-
ObjectId,
|
|
24
|
-
{},
|
|
25
|
-
Db,
|
|
26
|
-
Record<string, string>
|
|
27
|
-
> &
|
|
28
|
-
WithMongooseMeta<Db>;
|
|
29
|
-
|
|
30
|
-
export type LeanWithMeta<T> = WithMongooseMeta<T> & {
|
|
31
|
-
id: string;
|
|
32
|
-
createdAt: Date;
|
|
33
|
-
updatedAt: Date;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type LeanDocument<T> = T & {
|
|
37
|
-
id: string;
|
|
38
|
-
createdAt: Date;
|
|
39
|
-
updatedAt: Date;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type MongooseDoc<Db> = IfAny<
|
|
43
|
-
MongooseDocument<Db>,
|
|
44
|
-
MongooseDocument<Db>,
|
|
45
|
-
MongooseDocument<Db>
|
|
46
|
-
>;
|
|
47
|
-
|
|
48
|
-
export type ToLeanInput<D, T> =
|
|
49
|
-
| MongooseDoc<D>
|
|
50
|
-
| LeanDocument<T>
|
|
51
|
-
| null
|
|
52
|
-
| undefined;
|
|
53
|
-
|
|
54
|
-
// D - Document, meant for Database representation
|
|
55
|
-
// T - Type, meant for TypeScript representation
|
|
56
|
-
export type Repository<D, T> = {
|
|
57
|
-
model: Model<D>;
|
|
58
|
-
|
|
59
|
-
toLean(input: ToLeanInput<D, T>): LeanDocument<T> | null;
|
|
60
|
-
findById(id: string): Promise<LeanDocument<T> | null>;
|
|
61
|
-
create(entity: D): Promise<LeanDocument<T>>;
|
|
62
|
-
update(id: string, update: Partial<D>): Promise<LeanDocument<T> | null>;
|
|
63
|
-
delete(id: string): Promise<LeanDocument<T> | null>;
|
|
64
|
-
};
|
|
1
|
+
import { Model as MongooseModel } from "mongoose";
|
|
2
|
+
import { ModelCtor } from "sequelize";
|
|
3
|
+
import {
|
|
4
|
+
BaseRepositoryConfig,
|
|
5
|
+
Repository,
|
|
6
|
+
LeanDocument,
|
|
7
|
+
ToLeanInput,
|
|
8
|
+
MongooseRepositoryConfig,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import { MongooseRepository } from "./mongoose";
|
|
11
|
+
import { SequelizeRepository } from "./sequelize";
|
|
12
|
+
|
|
13
|
+
export * from "./types";
|
|
65
14
|
|
|
66
15
|
export abstract class BaseRepository<D, T> implements Repository<D, T> {
|
|
67
|
-
|
|
68
|
-
private readonly config: BaseRepositoryConfig<D>;
|
|
16
|
+
private implementation: Repository<D, T>;
|
|
69
17
|
|
|
70
18
|
constructor(config: BaseRepositoryConfig<D>) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (name) {
|
|
79
|
-
this.model = mongoose.model<D>(name, schema);
|
|
19
|
+
if (config.type === "mongoose") {
|
|
20
|
+
this.implementation = new MongooseRepository(config);
|
|
21
|
+
} else if (config.type === "sequelize") {
|
|
22
|
+
this.implementation = new SequelizeRepository(
|
|
23
|
+
config as any
|
|
24
|
+
) as unknown as Repository<D, T>;
|
|
80
25
|
} else {
|
|
81
|
-
|
|
26
|
+
// Backward compatibility: if type is missing but schema is present, assume Mongoose
|
|
27
|
+
if ((config as any).schema) {
|
|
28
|
+
const mongooseConfig: MongooseRepositoryConfig<D> = {
|
|
29
|
+
type: "mongoose",
|
|
30
|
+
name: (config as any).name,
|
|
31
|
+
schema: (config as any).schema,
|
|
32
|
+
};
|
|
33
|
+
this.implementation = new MongooseRepository(mongooseConfig);
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Invalid repository configuration: 'type' must be 'mongoose' or 'sequelize'"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
82
39
|
}
|
|
40
|
+
}
|
|
83
41
|
|
|
84
|
-
|
|
85
|
-
this.model
|
|
86
|
-
this.model.init();
|
|
42
|
+
get model(): MongooseModel<D> | ModelCtor<any> {
|
|
43
|
+
return this.implementation.model;
|
|
87
44
|
}
|
|
88
45
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*/
|
|
93
|
-
public toLean(input: ToLeanInput<D, T>): LeanDocument<T> | null {
|
|
94
|
-
if (!input) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
46
|
+
public isMongoose(): boolean {
|
|
47
|
+
return this.implementation instanceof MongooseRepository;
|
|
48
|
+
}
|
|
97
49
|
|
|
98
|
-
|
|
50
|
+
public isSequelize(): boolean {
|
|
51
|
+
return this.implementation instanceof SequelizeRepository;
|
|
52
|
+
}
|
|
99
53
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
104
|
-
} else {
|
|
105
|
-
base = input as LeanWithMeta<T>;
|
|
54
|
+
public getMongooseModel(): MongooseModel<D> {
|
|
55
|
+
if (!this.isMongoose()) {
|
|
56
|
+
throw new Error("Repository is not using Mongoose");
|
|
106
57
|
}
|
|
58
|
+
return this.implementation.model as MongooseModel<D>;
|
|
59
|
+
}
|
|
107
60
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
? base._id
|
|
115
|
-
: base._id !== undefined
|
|
116
|
-
? String(base._id)
|
|
117
|
-
: "",
|
|
118
|
-
createdAt:
|
|
119
|
-
base.createdAt instanceof Date
|
|
120
|
-
? base.createdAt
|
|
121
|
-
: base.createdAt
|
|
122
|
-
? new Date(base.createdAt as string | number)
|
|
123
|
-
: new Date(),
|
|
124
|
-
updatedAt:
|
|
125
|
-
base.updatedAt instanceof Date
|
|
126
|
-
? base.updatedAt
|
|
127
|
-
: base.updatedAt
|
|
128
|
-
? new Date(base.updatedAt as string | number)
|
|
129
|
-
: new Date(),
|
|
130
|
-
};
|
|
61
|
+
public getSequelizeModel(): ModelCtor<any> {
|
|
62
|
+
if (!this.isSequelize()) {
|
|
63
|
+
throw new Error("Repository is not using Sequelize");
|
|
64
|
+
}
|
|
65
|
+
return this.implementation.model as ModelCtor<any>;
|
|
66
|
+
}
|
|
131
67
|
|
|
132
|
-
|
|
68
|
+
public toLean(input: ToLeanInput<D, T>): LeanDocument<T> | null {
|
|
69
|
+
return this.implementation.toLean(input);
|
|
133
70
|
}
|
|
134
71
|
|
|
135
72
|
async findById(id: string): Promise<LeanDocument<T> | null> {
|
|
136
|
-
|
|
137
|
-
.findById(id)
|
|
138
|
-
.lean<LeanDocument<T>>({ virtuals: true })
|
|
139
|
-
.exec();
|
|
140
|
-
|
|
141
|
-
return this.toLean(entity);
|
|
73
|
+
return this.implementation.findById(id);
|
|
142
74
|
}
|
|
143
75
|
|
|
144
76
|
async create(entity: D): Promise<LeanDocument<T>> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const leanDocument = this.toLean(document as MongooseDoc<D>);
|
|
148
|
-
|
|
149
|
-
if (!leanDocument) {
|
|
150
|
-
throw new Error("failed to create document");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return leanDocument;
|
|
77
|
+
return this.implementation.create(entity);
|
|
154
78
|
}
|
|
155
79
|
|
|
156
80
|
async update(
|
|
157
81
|
id: string,
|
|
158
82
|
update: Partial<D>
|
|
159
83
|
): Promise<LeanDocument<T> | null> {
|
|
160
|
-
|
|
161
|
-
.findByIdAndUpdate(id, update, { new: true })
|
|
162
|
-
.lean<LeanDocument<T>>({ virtuals: true })
|
|
163
|
-
.exec();
|
|
164
|
-
|
|
165
|
-
return this.toLean(entity);
|
|
84
|
+
return this.implementation.update(id, update);
|
|
166
85
|
}
|
|
167
86
|
|
|
168
87
|
async delete(id: string): Promise<LeanDocument<T> | null> {
|
|
169
|
-
|
|
170
|
-
.findByIdAndDelete(id)
|
|
171
|
-
.lean<LeanDocument<T>>({ virtuals: true })
|
|
172
|
-
.exec();
|
|
173
|
-
|
|
174
|
-
return this.toLean(entity);
|
|
88
|
+
return this.implementation.delete(id);
|
|
175
89
|
}
|
|
176
90
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import importedMongoose, { connection, Model, Mongoose } from "mongoose";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { container } from "tsyringe";
|
|
4
|
+
import {
|
|
5
|
+
Repository,
|
|
6
|
+
MongooseRepositoryConfig,
|
|
7
|
+
LeanDocument,
|
|
8
|
+
ToLeanInput,
|
|
9
|
+
LeanWithMeta,
|
|
10
|
+
MongooseDoc,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export class MongooseRepository<D, T> implements Repository<D, T> {
|
|
14
|
+
public readonly model: Model<D>;
|
|
15
|
+
private readonly config: MongooseRepositoryConfig<D>;
|
|
16
|
+
|
|
17
|
+
constructor(config: MongooseRepositoryConfig<D>) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
const mongoose = connection.db?.databaseName
|
|
20
|
+
? importedMongoose
|
|
21
|
+
: container.resolve<Mongoose>(Mongoose);
|
|
22
|
+
|
|
23
|
+
const { name, schema } = this.config;
|
|
24
|
+
|
|
25
|
+
if (name) {
|
|
26
|
+
this.model = mongoose.model<D>(name, schema);
|
|
27
|
+
} else {
|
|
28
|
+
this.model = mongoose.model<D>(uuidv4(), schema);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.model.createCollection();
|
|
32
|
+
this.model.createIndexes();
|
|
33
|
+
this.model.init();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public toLean(input: ToLeanInput<D, T>): LeanDocument<T> | null {
|
|
37
|
+
if (!input) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let base: LeanWithMeta<T>;
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
input &&
|
|
45
|
+
typeof input === "object" &&
|
|
46
|
+
"toObject" in input &&
|
|
47
|
+
typeof (input as any).toObject === "function"
|
|
48
|
+
) {
|
|
49
|
+
base = (input as any).toObject({
|
|
50
|
+
virtuals: true,
|
|
51
|
+
}) as LeanWithMeta<T>;
|
|
52
|
+
} else {
|
|
53
|
+
base = input as LeanWithMeta<T>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result: LeanDocument<T> = {
|
|
57
|
+
...(base as T),
|
|
58
|
+
id:
|
|
59
|
+
typeof base.id === "string"
|
|
60
|
+
? base.id
|
|
61
|
+
: typeof base._id === "string"
|
|
62
|
+
? base._id
|
|
63
|
+
: base._id !== undefined
|
|
64
|
+
? String(base._id)
|
|
65
|
+
: "",
|
|
66
|
+
createdAt:
|
|
67
|
+
base.createdAt instanceof Date
|
|
68
|
+
? base.createdAt
|
|
69
|
+
: base.createdAt
|
|
70
|
+
? new Date(base.createdAt as string | number)
|
|
71
|
+
: new Date(),
|
|
72
|
+
updatedAt:
|
|
73
|
+
base.updatedAt instanceof Date
|
|
74
|
+
? base.updatedAt
|
|
75
|
+
: base.updatedAt
|
|
76
|
+
? new Date(base.updatedAt as string | number)
|
|
77
|
+
: new Date(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async findById(id: string): Promise<LeanDocument<T> | null> {
|
|
84
|
+
const entity = await this.model
|
|
85
|
+
.findById(id)
|
|
86
|
+
.lean<LeanDocument<T>>({ virtuals: true })
|
|
87
|
+
.exec();
|
|
88
|
+
|
|
89
|
+
return this.toLean(entity);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async create(entity: D): Promise<LeanDocument<T>> {
|
|
93
|
+
const document = await this.model.create<D>(entity);
|
|
94
|
+
|
|
95
|
+
const leanDocument = this.toLean(document as MongooseDoc<D>);
|
|
96
|
+
|
|
97
|
+
if (!leanDocument) {
|
|
98
|
+
throw new Error("failed to create document");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return leanDocument;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async update(
|
|
105
|
+
id: string,
|
|
106
|
+
update: Partial<D>
|
|
107
|
+
): Promise<LeanDocument<T> | null> {
|
|
108
|
+
const entity = await this.model
|
|
109
|
+
.findByIdAndUpdate(id, update, { new: true })
|
|
110
|
+
.lean<LeanDocument<T>>({ virtuals: true })
|
|
111
|
+
.exec();
|
|
112
|
+
|
|
113
|
+
return this.toLean(entity);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async delete(id: string): Promise<LeanDocument<T> | null> {
|
|
117
|
+
const entity = await this.model
|
|
118
|
+
.findByIdAndDelete(id)
|
|
119
|
+
.lean<LeanDocument<T>>({ virtuals: true })
|
|
120
|
+
.exec();
|
|
121
|
+
|
|
122
|
+
return this.toLean(entity);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Model, ModelCtor } from "sequelize";
|
|
2
|
+
import { Repository, SequelizeRepositoryConfig, LeanDocument } from "./types";
|
|
3
|
+
|
|
4
|
+
export class SequelizeRepository<D extends Model, T>
|
|
5
|
+
implements Repository<D, T>
|
|
6
|
+
{
|
|
7
|
+
public readonly model: ModelCtor<D>;
|
|
8
|
+
|
|
9
|
+
constructor(config: SequelizeRepositoryConfig<D>) {
|
|
10
|
+
this.model = config.model;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public toLean(input: any): LeanDocument<T> | null {
|
|
14
|
+
if (!input) return null;
|
|
15
|
+
const plain = input instanceof Model ? input.get({ plain: true }) : input;
|
|
16
|
+
|
|
17
|
+
// Ensure id, createdAt, updatedAt exist and are correct types
|
|
18
|
+
// This assumes the model has these fields.
|
|
19
|
+
return {
|
|
20
|
+
...(plain as T),
|
|
21
|
+
id: String(plain.id || plain._id || ""),
|
|
22
|
+
createdAt: plain.createdAt ? new Date(plain.createdAt) : new Date(),
|
|
23
|
+
updatedAt: plain.updatedAt ? new Date(plain.updatedAt) : new Date(),
|
|
24
|
+
} as LeanDocument<T>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async findById(id: string): Promise<LeanDocument<T> | null> {
|
|
28
|
+
const entity = await this.model.findByPk(id);
|
|
29
|
+
return this.toLean(entity);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async create(entity: D): Promise<LeanDocument<T>> {
|
|
33
|
+
const document = await this.model.create(entity as any);
|
|
34
|
+
return this.toLean(document) as LeanDocument<T>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async update(
|
|
38
|
+
id: string,
|
|
39
|
+
update: Partial<D>
|
|
40
|
+
): Promise<LeanDocument<T> | null> {
|
|
41
|
+
const [affectedCount] = await this.model.update(update as any, {
|
|
42
|
+
where: { id } as any,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (affectedCount === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.findById(id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async delete(id: string): Promise<LeanDocument<T> | null> {
|
|
53
|
+
const entity = await this.findById(id);
|
|
54
|
+
if (!entity) return null;
|
|
55
|
+
|
|
56
|
+
await this.model.destroy({
|
|
57
|
+
where: { id } as any,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return entity;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
[
|
|
64
|
+
{
|
|
65
|
+
type: "image",
|
|
66
|
+
payload: {
|
|
67
|
+
attachment_id: "{{ $json.propertyName }}",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Document,
|
|
3
|
+
Model as MongooseModel,
|
|
4
|
+
Schema,
|
|
5
|
+
Default__v,
|
|
6
|
+
Require_id,
|
|
7
|
+
ObjectId,
|
|
8
|
+
IfAny,
|
|
9
|
+
} from "mongoose";
|
|
10
|
+
import { Model as SequelizeModel, ModelCtor, Model } from "sequelize";
|
|
11
|
+
|
|
12
|
+
export type WithMongooseMeta<T> = Default__v<Require_id<T>>;
|
|
13
|
+
|
|
14
|
+
export type MongooseDocument<Db> = Document<
|
|
15
|
+
ObjectId,
|
|
16
|
+
{},
|
|
17
|
+
Db,
|
|
18
|
+
Record<string, string>
|
|
19
|
+
> &
|
|
20
|
+
WithMongooseMeta<Db>;
|
|
21
|
+
|
|
22
|
+
export type LeanWithMeta<T> = WithMongooseMeta<T> & {
|
|
23
|
+
id: string;
|
|
24
|
+
createdAt: Date;
|
|
25
|
+
updatedAt: Date;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type LeanDocument<T> = T & {
|
|
29
|
+
id: string;
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
updatedAt: Date;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type MongooseDoc<Db> = IfAny<
|
|
35
|
+
MongooseDocument<Db>,
|
|
36
|
+
MongooseDocument<Db>,
|
|
37
|
+
MongooseDocument<Db>
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
export type ToLeanInput<D, T> =
|
|
41
|
+
| MongooseDoc<D>
|
|
42
|
+
| LeanDocument<T>
|
|
43
|
+
| null
|
|
44
|
+
| undefined;
|
|
45
|
+
|
|
46
|
+
export type MongooseRepositoryConfig<D> = {
|
|
47
|
+
type: "mongoose";
|
|
48
|
+
name?: string;
|
|
49
|
+
schema: Schema<D>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type SequelizeRepositoryConfig<D extends Model> = {
|
|
53
|
+
type: "sequelize";
|
|
54
|
+
model: ModelCtor<D>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type BaseRepositoryConfig<D> =
|
|
58
|
+
| MongooseRepositoryConfig<D>
|
|
59
|
+
| SequelizeRepositoryConfig<D extends Model ? D : never>;
|
|
60
|
+
|
|
61
|
+
export type Repository<D, T> = {
|
|
62
|
+
model: MongooseModel<D> | ModelCtor<SequelizeModel<any, any>>;
|
|
63
|
+
|
|
64
|
+
toLean(input: any): LeanDocument<T> | null;
|
|
65
|
+
findById(id: string): Promise<LeanDocument<T> | null>;
|
|
66
|
+
create(entity: D): Promise<LeanDocument<T>>;
|
|
67
|
+
update(id: string, update: Partial<D>): Promise<LeanDocument<T> | null>;
|
|
68
|
+
delete(id: string): Promise<LeanDocument<T> | null>;
|
|
69
|
+
};
|
|
@@ -3,6 +3,7 @@ import { FastifyInstance } from "fastify";
|
|
|
3
3
|
import { ZodTypeProvider } from "fastify-type-provider-zod";
|
|
4
4
|
import { container } from "tsyringe";
|
|
5
5
|
import { Mongoose } from "mongoose";
|
|
6
|
+
import { Sequelize } from "sequelize";
|
|
6
7
|
|
|
7
8
|
import { HookHandler, RouteHandler } from "../../application";
|
|
8
9
|
import { CustomError } from "../../../../error";
|
|
@@ -37,9 +38,10 @@ type ServiceServer = {
|
|
|
37
38
|
|
|
38
39
|
export enum DatabaseVendor {
|
|
39
40
|
MONGOOSE = "MONGOOSE",
|
|
41
|
+
SEQUELIZE = "SEQUELIZE",
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
type DatabaseInstance = Mongoose;
|
|
44
|
+
type DatabaseInstance = Mongoose | Sequelize;
|
|
43
45
|
|
|
44
46
|
type OnDatabaseConnectedFunction = Optional<
|
|
45
47
|
(dbInstance: DatabaseInstance) => Promise<void> | void
|
|
@@ -126,7 +128,9 @@ export const strategiesForDatabaseVendor = {
|
|
|
126
128
|
const { serviceName, database, connectionString, onDatabaseConnected } =
|
|
127
129
|
input;
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
const mongoose = database as Mongoose;
|
|
132
|
+
|
|
133
|
+
if (!mongoose || !mongoose.connect) {
|
|
130
134
|
throw new CustomError({
|
|
131
135
|
message: "database instance is not provided",
|
|
132
136
|
code: 500,
|
|
@@ -134,18 +138,45 @@ export const strategiesForDatabaseVendor = {
|
|
|
134
138
|
});
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
await
|
|
141
|
+
await mongoose.connect(connectionString, {
|
|
138
142
|
dbName: serviceName,
|
|
139
143
|
autoIndex: true,
|
|
140
144
|
autoCreate: true,
|
|
141
145
|
});
|
|
142
146
|
|
|
147
|
+
if (onDatabaseConnected) {
|
|
148
|
+
await onDatabaseConnected(mongoose);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
container.register<Mongoose>(mongoose.constructor.name, {
|
|
152
|
+
useValue: mongoose,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
[DatabaseVendor.SEQUELIZE]: async (input: {
|
|
156
|
+
serviceName: string;
|
|
157
|
+
database: DatabaseInstance;
|
|
158
|
+
connectionString: string;
|
|
159
|
+
onDatabaseConnected?: OnDatabaseConnectedFunction;
|
|
160
|
+
}) => {
|
|
161
|
+
const { database, onDatabaseConnected } = input;
|
|
162
|
+
const sequelize = database as Sequelize;
|
|
163
|
+
|
|
164
|
+
if (!sequelize || !sequelize.authenticate) {
|
|
165
|
+
throw new CustomError({
|
|
166
|
+
message: "database instance is not provided or invalid",
|
|
167
|
+
code: 500,
|
|
168
|
+
reason: "database instance not provided",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await sequelize.authenticate();
|
|
173
|
+
|
|
143
174
|
if (onDatabaseConnected) {
|
|
144
175
|
await onDatabaseConnected(database);
|
|
145
176
|
}
|
|
146
177
|
|
|
147
|
-
container.register<
|
|
148
|
-
useValue:
|
|
178
|
+
container.register<Sequelize>("Sequelize", {
|
|
179
|
+
useValue: sequelize,
|
|
149
180
|
});
|
|
150
181
|
},
|
|
151
182
|
};
|
|
@@ -15,7 +15,7 @@ const schemaUserTest = new Schema<UserTest>({
|
|
|
15
15
|
@singleton()
|
|
16
16
|
class UserRepository extends BaseRepository<UserTest, UserTest> {
|
|
17
17
|
constructor() {
|
|
18
|
-
super({ name: "User", schema: schemaUserTest });
|
|
18
|
+
super({ type: "mongoose", name: "User", schema: schemaUserTest });
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -29,14 +29,14 @@ describe("BaseRepository", () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it("should create a model using the provided name and schema", async () => {
|
|
32
|
-
expect(repository.
|
|
33
|
-
expect(repository.
|
|
32
|
+
expect(repository.getMongooseModel().modelName).toBe("User");
|
|
33
|
+
expect(repository.getMongooseModel().schema).toBe(schemaUserTest);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("should call create() and return the created user document", async () => {
|
|
37
37
|
const user = { name: "Alice" };
|
|
38
38
|
|
|
39
|
-
const spyFunction = vi.spyOn(repository.
|
|
39
|
+
const spyFunction = vi.spyOn(repository.getMongooseModel(), "create");
|
|
40
40
|
|
|
41
41
|
const result = await repository.create(user);
|
|
42
42
|
|
|
@@ -46,7 +46,7 @@ describe("BaseRepository", () => {
|
|
|
46
46
|
|
|
47
47
|
it("should call findById() and return the user document", async () => {
|
|
48
48
|
const user = await repository.create({ name: "Alice" });
|
|
49
|
-
const spyFunction = vi.spyOn(repository.
|
|
49
|
+
const spyFunction = vi.spyOn(repository.getMongooseModel(), "findById");
|
|
50
50
|
const result = await repository.findById(user.id);
|
|
51
51
|
expect(spyFunction).toHaveBeenNthCalledWith(1, user.id);
|
|
52
52
|
expect(result).toMatchObject({ name: user.name });
|
|
@@ -58,7 +58,10 @@ describe("BaseRepository", () => {
|
|
|
58
58
|
|
|
59
59
|
const update = { name: "Alice Updated" };
|
|
60
60
|
|
|
61
|
-
const spyFunction = vi.spyOn(
|
|
61
|
+
const spyFunction = vi.spyOn(
|
|
62
|
+
repository.getMongooseModel(),
|
|
63
|
+
"findByIdAndUpdate"
|
|
64
|
+
);
|
|
62
65
|
|
|
63
66
|
const result = await repository.update(user.id, update);
|
|
64
67
|
|
|
@@ -70,7 +73,10 @@ describe("BaseRepository", () => {
|
|
|
70
73
|
|
|
71
74
|
it("should call delete() and return the deleted user document", async () => {
|
|
72
75
|
const user = await repository.create({ name: "Alice" });
|
|
73
|
-
const spyFunction = vi.spyOn(
|
|
76
|
+
const spyFunction = vi.spyOn(
|
|
77
|
+
repository.getMongooseModel(),
|
|
78
|
+
"findByIdAndDelete"
|
|
79
|
+
);
|
|
74
80
|
const result = await repository.delete(user.id);
|
|
75
81
|
expect(spyFunction).toHaveBeenNthCalledWith(1, user.id);
|
|
76
82
|
// Patch: allow for id only (ignore _id)
|