@sentzunhat/zacatl 0.0.12 → 0.0.13
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 +6 -2
- package/src/micro-service/architecture/application/entry-points/rest/common/handler.ts +3 -2
- package/src/micro-service/architecture/domain/domain.ts +1 -1
- package/src/micro-service/architecture/infrastructure/infrastructure.ts +1 -1
- package/src/micro-service/architecture/infrastructure/repositories/abstract.ts +41 -149
- 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 +7 -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.13",
|
|
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
|
},
|
|
@@ -4,8 +4,12 @@ import path from "path";
|
|
|
4
4
|
import { AbstractArchitecture } from "../architecture";
|
|
5
5
|
import { HookHandler, RouteHandler } from "./entry-points/rest";
|
|
6
6
|
|
|
7
|
-
export type ApplicationHookHandlers = Array<
|
|
8
|
-
|
|
7
|
+
export type ApplicationHookHandlers = Array<
|
|
8
|
+
new (...args: unknown[]) => HookHandler
|
|
9
|
+
>;
|
|
10
|
+
export type ApplicationRouteHandlers = Array<
|
|
11
|
+
new (...args: unknown[]) => RouteHandler
|
|
12
|
+
>;
|
|
9
13
|
|
|
10
14
|
export type ApplicationEntryPoints = {
|
|
11
15
|
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>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AbstractArchitecture } from "../architecture";
|
|
2
2
|
|
|
3
3
|
export type ConfigInfrastructure = {
|
|
4
|
-
repositories: Array<new () => unknown>;
|
|
4
|
+
repositories: Array<new (...args: unknown[]) => unknown>;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
export class Infrastructure extends AbstractArchitecture {
|
|
@@ -1,176 +1,68 @@
|
|
|
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
|
-
* Ensures the returned document has the correct id, createdAt, and updatedAt fields.
|
|
91
|
-
* Accepts a Mongoose document, a plain object, or a result from .lean().
|
|
92
|
-
*/
|
|
93
46
|
public toLean(input: ToLeanInput<D, T>): LeanDocument<T> | null {
|
|
94
|
-
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let base: LeanWithMeta<T>;
|
|
99
|
-
|
|
100
|
-
if (typeof (input as MongooseDoc<D>).toObject === "function") {
|
|
101
|
-
base = (input as MongooseDoc<D>).toObject<LeanDocument<T>>({
|
|
102
|
-
virtuals: true,
|
|
103
|
-
});
|
|
104
|
-
} else {
|
|
105
|
-
base = input as LeanWithMeta<T>;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const result: LeanDocument<T> = {
|
|
109
|
-
...(base as T), // trust only known keys from T
|
|
110
|
-
id:
|
|
111
|
-
typeof base.id === "string"
|
|
112
|
-
? base.id
|
|
113
|
-
: typeof base._id === "string"
|
|
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
|
-
};
|
|
131
|
-
|
|
132
|
-
return result;
|
|
47
|
+
return this.implementation.toLean(input);
|
|
133
48
|
}
|
|
134
49
|
|
|
135
50
|
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);
|
|
51
|
+
return this.implementation.findById(id);
|
|
142
52
|
}
|
|
143
53
|
|
|
144
54
|
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;
|
|
55
|
+
return this.implementation.create(entity);
|
|
154
56
|
}
|
|
155
57
|
|
|
156
58
|
async update(
|
|
157
59
|
id: string,
|
|
158
60
|
update: Partial<D>
|
|
159
61
|
): 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);
|
|
62
|
+
return this.implementation.update(id, update);
|
|
166
63
|
}
|
|
167
64
|
|
|
168
65
|
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);
|
|
66
|
+
return this.implementation.delete(id);
|
|
175
67
|
}
|
|
176
68
|
}
|
|
@@ -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.model.modelName).toBe("User");
|
|
33
|
-
expect(repository.model.schema).toBe(schemaUserTest);
|
|
32
|
+
expect((repository.model as any).modelName).toBe("User");
|
|
33
|
+
expect((repository.model as any).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.model, "create");
|
|
39
|
+
const spyFunction = vi.spyOn(repository.model as any, "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.model, "findById");
|
|
49
|
+
const spyFunction = vi.spyOn(repository.model as any, "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,7 @@ describe("BaseRepository", () => {
|
|
|
58
58
|
|
|
59
59
|
const update = { name: "Alice Updated" };
|
|
60
60
|
|
|
61
|
-
const spyFunction = vi.spyOn(repository.model, "findByIdAndUpdate");
|
|
61
|
+
const spyFunction = vi.spyOn(repository.model as any, "findByIdAndUpdate");
|
|
62
62
|
|
|
63
63
|
const result = await repository.update(user.id, update);
|
|
64
64
|
|
|
@@ -70,7 +70,7 @@ describe("BaseRepository", () => {
|
|
|
70
70
|
|
|
71
71
|
it("should call delete() and return the deleted user document", async () => {
|
|
72
72
|
const user = await repository.create({ name: "Alice" });
|
|
73
|
-
const spyFunction = vi.spyOn(repository.model, "findByIdAndDelete");
|
|
73
|
+
const spyFunction = vi.spyOn(repository.model as any, "findByIdAndDelete");
|
|
74
74
|
const result = await repository.delete(user.id);
|
|
75
75
|
expect(spyFunction).toHaveBeenNthCalledWith(1, user.id);
|
|
76
76
|
// Patch: allow for id only (ignore _id)
|