@twasik4/pocket-service 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +637 -297
- package/dist/index.d.mts +747 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime/worker-thread.d.mts +1 -0
- package/dist/runtime/worker-thread.mjs +2 -0
- package/dist/runtime/worker-thread.mjs.map +1 -0
- package/package.json +53 -10
- package/dist/index.d.ts +0 -702
- package/dist/index.js +0 -6
- package/dist/index.js.map +0 -1
- package/dist/worker-thread.d.ts +0 -2
- package/dist/worker-thread.js +0 -2
- package/dist/worker-thread.js.map +0 -1
- package/src/index.ts +0 -1
- package/src/worker-thread.ts +0 -159
- package/src/workers/auth/strategies.ts +0 -304
- package/src/workers/db/clickhouse/index.ts +0 -76
- package/src/workers/db/mongo/collection.ts +0 -292
- package/src/workers/db/mongo/mongo.ts +0 -285
- package/src/workers/express-types.ts +0 -442
- package/src/workers/index.ts +0 -7
- package/src/workers/logger.ts +0 -19
- package/src/workers/service.ts +0 -1015
- package/src/workers/stream-handler.ts +0 -25
- package/src/workers/types.ts +0 -59
- package/src/workers/utils.ts +0 -19
- package/tests/mongo.spec.ts +0 -92
- package/tests/redis.spec.ts +0 -36
- package/tests/service.spec.ts +0 -82
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -11
- package/vitest.config.ts +0 -8
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { Request } from "express";
|
|
2
|
-
import { createRemoteJWKSet, JWTPayload, jwtVerify } from "jose";
|
|
3
|
-
import { PeerCertificate, TLSSocket } from "tls";
|
|
4
|
-
import {
|
|
5
|
-
AuthenticatedUser,
|
|
6
|
-
AuthStrategy,
|
|
7
|
-
RouteDefinition,
|
|
8
|
-
} from "../express-types";
|
|
9
|
-
|
|
10
|
-
type JwtClaimValue = string | string[] | number | boolean | null | undefined;
|
|
11
|
-
|
|
12
|
-
export type JwtStrategyClaims = {
|
|
13
|
-
sub?: string;
|
|
14
|
-
[key: string]: unknown;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type JwtTokenVerifier = (
|
|
18
|
-
token: string,
|
|
19
|
-
req: Request,
|
|
20
|
-
route: RouteDefinition<any, any, boolean>,
|
|
21
|
-
) => JwtStrategyClaims | null | Promise<JwtStrategyClaims | null>;
|
|
22
|
-
|
|
23
|
-
export type CreateJwtAuthStrategyOptions = {
|
|
24
|
-
name?: string;
|
|
25
|
-
headerName?: string;
|
|
26
|
-
tokenPrefix?: string;
|
|
27
|
-
verifyToken: JwtTokenVerifier;
|
|
28
|
-
mapPayloadToUser?: (
|
|
29
|
-
claims: JwtStrategyClaims,
|
|
30
|
-
req: Request,
|
|
31
|
-
route: RouteDefinition<any, any, boolean>,
|
|
32
|
-
) => AuthenticatedUser | null | Promise<AuthenticatedUser | null>;
|
|
33
|
-
canHandle?: AuthStrategy["canHandle"];
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type CreateJoseJwtVerifierOptions = {
|
|
37
|
-
jwksUri?: string;
|
|
38
|
-
secret?: string | Uint8Array;
|
|
39
|
-
issuer?: string | string[];
|
|
40
|
-
audience?: string | string[];
|
|
41
|
-
algorithms?: string[];
|
|
42
|
-
clockTolerance?: string | number;
|
|
43
|
-
requiredClaims?: string[];
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export type CreateMtlsAuthStrategyOptions = {
|
|
47
|
-
name?: string;
|
|
48
|
-
requireAuthorized?: boolean;
|
|
49
|
-
mapCertificateToUser?: (
|
|
50
|
-
certificate: PeerCertificate,
|
|
51
|
-
req: Request,
|
|
52
|
-
route: RouteDefinition<any, any, boolean>,
|
|
53
|
-
) => AuthenticatedUser | null | Promise<AuthenticatedUser | null>;
|
|
54
|
-
canHandle?: AuthStrategy["canHandle"];
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
function readAuthTokenFromHeader(
|
|
58
|
-
req: Request,
|
|
59
|
-
headerName: string,
|
|
60
|
-
tokenPrefix: string,
|
|
61
|
-
): string | null {
|
|
62
|
-
const header = req.headers[headerName.toLowerCase()];
|
|
63
|
-
|
|
64
|
-
if (typeof header !== "string") {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const value = header.trim();
|
|
69
|
-
if (!value) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!tokenPrefix) {
|
|
74
|
-
return value;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const prefix = `${tokenPrefix} `;
|
|
78
|
-
if (!value.startsWith(prefix)) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return value.slice(prefix.length).trim() || null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isUserValue(
|
|
86
|
-
value: unknown,
|
|
87
|
-
): value is Exclude<JwtClaimValue, null | undefined> {
|
|
88
|
-
return (
|
|
89
|
-
typeof value === "string" ||
|
|
90
|
-
typeof value === "number" ||
|
|
91
|
-
typeof value === "boolean" ||
|
|
92
|
-
(Array.isArray(value) && value.every((v) => typeof v === "string"))
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function claimsToAuthenticatedUser(
|
|
97
|
-
claims: JwtStrategyClaims,
|
|
98
|
-
): AuthenticatedUser | null {
|
|
99
|
-
if (typeof claims.sub !== "string" || claims.sub.length === 0) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const user: AuthenticatedUser = { userId: claims.sub };
|
|
104
|
-
for (const [key, value] of Object.entries(claims)) {
|
|
105
|
-
if (key === "sub") {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (isUserValue(value)) {
|
|
110
|
-
user[key] = value;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return user;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function jwtPayloadToStrategyClaims(payload: JWTPayload): JwtStrategyClaims {
|
|
118
|
-
const claims: JwtStrategyClaims = {};
|
|
119
|
-
|
|
120
|
-
for (const [key, value] of Object.entries(payload)) {
|
|
121
|
-
claims[key] = value;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (typeof payload.sub === "string") {
|
|
125
|
-
claims.sub = payload.sub;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return claims;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function hasRequiredClaims(payload: JWTPayload, requiredClaims: string[]) {
|
|
132
|
-
return requiredClaims.every((claim) => payload[claim] !== undefined);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function createJoseJwtVerifier(
|
|
136
|
-
options: CreateJoseJwtVerifierOptions,
|
|
137
|
-
): JwtTokenVerifier {
|
|
138
|
-
const requiredClaims = options.requiredClaims ?? ["sub"];
|
|
139
|
-
const jwtVerifyOptions = {
|
|
140
|
-
issuer: options.issuer,
|
|
141
|
-
audience: options.audience,
|
|
142
|
-
algorithms: options.algorithms,
|
|
143
|
-
clockTolerance: options.clockTolerance,
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
if (options.jwksUri) {
|
|
147
|
-
const jwksKeySource = createRemoteJWKSet(new URL(options.jwksUri));
|
|
148
|
-
|
|
149
|
-
return async (token) => {
|
|
150
|
-
try {
|
|
151
|
-
const { payload } = await jwtVerify(
|
|
152
|
-
token,
|
|
153
|
-
jwksKeySource,
|
|
154
|
-
jwtVerifyOptions,
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
if (!hasRequiredClaims(payload, requiredClaims)) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return jwtPayloadToStrategyClaims(payload);
|
|
162
|
-
} catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const secretKey =
|
|
169
|
-
typeof options.secret === "string"
|
|
170
|
-
? new TextEncoder().encode(options.secret)
|
|
171
|
-
: options.secret;
|
|
172
|
-
|
|
173
|
-
if (!(secretKey instanceof Uint8Array)) {
|
|
174
|
-
throw new Error("createJoseJwtVerifier requires either jwksUri or secret.");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return async (token) => {
|
|
178
|
-
try {
|
|
179
|
-
const { payload } = await jwtVerify(token, secretKey, jwtVerifyOptions);
|
|
180
|
-
|
|
181
|
-
if (!hasRequiredClaims(payload, requiredClaims)) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return jwtPayloadToStrategyClaims(payload);
|
|
186
|
-
} catch {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function parseSubjectAltNames(subjectAltName?: string): string[] {
|
|
193
|
-
if (!subjectAltName) {
|
|
194
|
-
return [];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return subjectAltName
|
|
198
|
-
.split(",")
|
|
199
|
-
.map((entry) => entry.trim())
|
|
200
|
-
.filter(Boolean);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function resolveMtlsIdentity(certificate: PeerCertificate): string | null {
|
|
204
|
-
const sans = parseSubjectAltNames(certificate.subjectaltname);
|
|
205
|
-
const uriSan = sans.find((value) => value.startsWith("URI:"));
|
|
206
|
-
if (uriSan) {
|
|
207
|
-
return uriSan.slice(4).trim() || null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const dnsSan = sans.find((value) => value.startsWith("DNS:"));
|
|
211
|
-
if (dnsSan) {
|
|
212
|
-
return dnsSan.slice(4).trim() || null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const subjectCn = certificate.subject?.CN;
|
|
216
|
-
if (typeof subjectCn === "string" && subjectCn.length > 0) {
|
|
217
|
-
return subjectCn;
|
|
218
|
-
}
|
|
219
|
-
if (Array.isArray(subjectCn) && subjectCn.length > 0) {
|
|
220
|
-
const first = subjectCn[0];
|
|
221
|
-
if (typeof first === "string" && first.length > 0) {
|
|
222
|
-
return first;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (certificate.fingerprint256) {
|
|
227
|
-
return certificate.fingerprint256;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function createJwtAuthStrategy(
|
|
234
|
-
options: CreateJwtAuthStrategyOptions,
|
|
235
|
-
): AuthStrategy {
|
|
236
|
-
const headerName = options.headerName ?? "authorization";
|
|
237
|
-
const tokenPrefix = options.tokenPrefix ?? "Bearer";
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
name: options.name ?? "jwt",
|
|
241
|
-
canHandle: options.canHandle,
|
|
242
|
-
authenticate: async (req, route) => {
|
|
243
|
-
const token = readAuthTokenFromHeader(req, headerName, tokenPrefix);
|
|
244
|
-
if (!token) {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const claims = await options.verifyToken(token, req, route);
|
|
249
|
-
if (!claims) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (options.mapPayloadToUser) {
|
|
254
|
-
return options.mapPayloadToUser(claims, req, route);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return claimsToAuthenticatedUser(claims);
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export function createMtlsAuthStrategy(
|
|
263
|
-
options: CreateMtlsAuthStrategyOptions = {},
|
|
264
|
-
): AuthStrategy {
|
|
265
|
-
const requireAuthorized = options.requireAuthorized ?? true;
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
name: options.name ?? "mtls",
|
|
269
|
-
canHandle: options.canHandle,
|
|
270
|
-
authenticate: async (req, route) => {
|
|
271
|
-
const socket = req.socket as TLSSocket;
|
|
272
|
-
if (typeof socket.getPeerCertificate !== "function") {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (requireAuthorized && socket.authorized !== true) {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const certificate = socket.getPeerCertificate(true);
|
|
281
|
-
if (!certificate || Object.keys(certificate).length === 0) {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (options.mapCertificateToUser) {
|
|
286
|
-
return options.mapCertificateToUser(certificate, req, route);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const userId = resolveMtlsIdentity(certificate);
|
|
290
|
-
if (!userId) {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
userId,
|
|
296
|
-
certSubjectCn: certificate.subject?.CN ?? "",
|
|
297
|
-
certIssuerCn: certificate.issuer?.CN ?? "",
|
|
298
|
-
certFingerprint256: certificate.fingerprint256 ?? "",
|
|
299
|
-
certSerialNumber: certificate.serialNumber ?? "",
|
|
300
|
-
mtlsAuthorized: socket.authorized,
|
|
301
|
-
};
|
|
302
|
-
},
|
|
303
|
-
};
|
|
304
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { createClient } from "@clickhouse/client";
|
|
2
|
-
|
|
3
|
-
export interface ClickhouseTable<TSchema extends Record<string, any>> {
|
|
4
|
-
name: string;
|
|
5
|
-
createSQL: string;
|
|
6
|
-
schema: () => TSchema;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ClickhouseConfig<
|
|
10
|
-
TTables extends Record<string, ClickhouseTable<any>>
|
|
11
|
-
> {
|
|
12
|
-
url: string;
|
|
13
|
-
username?: string;
|
|
14
|
-
password?: string;
|
|
15
|
-
tables: TTables;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function createTypedClickhouse<
|
|
19
|
-
TTables extends Record<string, ClickhouseTable<any>>
|
|
20
|
-
>(config: ClickhouseConfig<TTables>) {
|
|
21
|
-
const client = createClient({
|
|
22
|
-
host: config.url,
|
|
23
|
-
username: config.username,
|
|
24
|
-
password: config.password,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
async function ensureTables() {
|
|
28
|
-
for (const { createSQL } of Object.values(config.tables || {})) {
|
|
29
|
-
await client.command({ query: createSQL });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const api = Object.fromEntries(
|
|
34
|
-
Object.entries(config.tables || {}).map(([key, table]) => {
|
|
35
|
-
type Row = ReturnType<typeof table.schema>;
|
|
36
|
-
type Filter = Partial<Row>;
|
|
37
|
-
|
|
38
|
-
const select = async (where?: Filter): Promise<Row[]> => {
|
|
39
|
-
let query = `SELECT * FROM ${table.name}`;
|
|
40
|
-
if (where && Object.keys(where).length > 0) {
|
|
41
|
-
const conditions = Object.keys(where)
|
|
42
|
-
.map(([k, value]) => {
|
|
43
|
-
if (typeof value === "string") return `${k} = '${value}'`;
|
|
44
|
-
return `${k} = ${value}`;
|
|
45
|
-
})
|
|
46
|
-
.join(" AND ");
|
|
47
|
-
query += ` WHERE ${conditions}`;
|
|
48
|
-
}
|
|
49
|
-
const res = await client.query({
|
|
50
|
-
query,
|
|
51
|
-
});
|
|
52
|
-
const rows = await res.json<{ data: any[] }>();
|
|
53
|
-
return rows.data as Row[];
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const insert = async (rows: Row[]) => {
|
|
57
|
-
await client.insert({
|
|
58
|
-
table: table.name,
|
|
59
|
-
values: rows,
|
|
60
|
-
format: "JSONEachRow",
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return [key, { select, insert }];
|
|
65
|
-
})
|
|
66
|
-
) as {
|
|
67
|
-
[K in keyof TTables]: {
|
|
68
|
-
select(
|
|
69
|
-
where?: Partial<ReturnType<TTables[K]["schema"]>>
|
|
70
|
-
): Promise<ReturnType<TTables[K]["schema"]>[]>;
|
|
71
|
-
insert(rows: ReturnType<TTables[K]["schema"]>[]): Promise<void>;
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return { client, ensureTables, ...api };
|
|
76
|
-
}
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Collection,
|
|
3
|
-
Document,
|
|
4
|
-
Filter,
|
|
5
|
-
ObjectId,
|
|
6
|
-
OptionalUnlessRequiredId,
|
|
7
|
-
UpdateFilter,
|
|
8
|
-
WithId,
|
|
9
|
-
} from "mongodb";
|
|
10
|
-
|
|
11
|
-
export type SchemaParser<T extends Document> = {
|
|
12
|
-
parse: (input: unknown) => T;
|
|
13
|
-
shape?: unknown;
|
|
14
|
-
_def?: {
|
|
15
|
-
shape?: unknown | (() => unknown);
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export class CollectionWrapper<T extends Document> {
|
|
20
|
-
constructor(
|
|
21
|
-
private readonly collection: Collection<T>,
|
|
22
|
-
private readonly schema?: SchemaParser<T>,
|
|
23
|
-
private readonly revision?: Collection<T>,
|
|
24
|
-
) {}
|
|
25
|
-
|
|
26
|
-
private parseWrite(doc: unknown): T {
|
|
27
|
-
if (!this.schema) {
|
|
28
|
-
return doc as T;
|
|
29
|
-
}
|
|
30
|
-
return this.schema.parse(doc);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private parseRead(doc: WithId<T>): WithId<T> {
|
|
34
|
-
if (!this.schema) {
|
|
35
|
-
return doc;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const parsed = this.schema.parse(doc) as T;
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
...(parsed as Document),
|
|
42
|
-
_id: doc._id,
|
|
43
|
-
} as WithId<T>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private async trackRevision(doc: T) {
|
|
47
|
-
if (this.revision) {
|
|
48
|
-
const clone = {
|
|
49
|
-
...doc,
|
|
50
|
-
_id: new ObjectId(),
|
|
51
|
-
} as OptionalUnlessRequiredId<T>;
|
|
52
|
-
await this.revision.insertOne(clone);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get all documents from the collection.
|
|
58
|
-
* @param filter - The filter to find documents to retrieve. If no filter is provided, it will return all documents in the collection.
|
|
59
|
-
* @returns
|
|
60
|
-
*/
|
|
61
|
-
async get(filter: Filter<T> = {}): Promise<WithId<T>[]> {
|
|
62
|
-
try {
|
|
63
|
-
if (this.hasProperty("deletedAt")) {
|
|
64
|
-
// If the schema has a `deletedAt` field, filter out soft-deleted documents
|
|
65
|
-
filter = { ...filter, deletedAt: { $exists: false } };
|
|
66
|
-
}
|
|
67
|
-
const result = await this.collection.find(filter).toArray();
|
|
68
|
-
return result.map((doc) => this.parseRead(doc));
|
|
69
|
-
} catch (err) {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get a single document from the collection.
|
|
76
|
-
* @param filter - The filter to find the document to retrieve. If no filter is provided, it will return the first document in the collection.
|
|
77
|
-
* @returns
|
|
78
|
-
*/
|
|
79
|
-
async getOne(filter: Filter<T> = {}): Promise<WithId<T> | null> {
|
|
80
|
-
try {
|
|
81
|
-
if (this.hasProperty("deletedAt")) {
|
|
82
|
-
// If the schema has a `deletedAt` field, filter out soft-deleted documents
|
|
83
|
-
filter = { ...filter, deletedAt: { $exists: false } };
|
|
84
|
-
}
|
|
85
|
-
const result = await this.collection.findOne(filter);
|
|
86
|
-
return result ? this.parseRead(result) : null;
|
|
87
|
-
} catch (err) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get all revisions for a document.
|
|
94
|
-
* @param filter - The filter to find revisions to retrieve. If no filter is provided, it will return all revisions in the revision collection.
|
|
95
|
-
* @returns An array of revision documents.
|
|
96
|
-
*/
|
|
97
|
-
async getRevisions(filter: Filter<T> = {}): Promise<WithId<T>[]> {
|
|
98
|
-
try {
|
|
99
|
-
if (!this.revision) {
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
const result = await this.revision
|
|
103
|
-
.find(filter, { sort: { revision: 1 } })
|
|
104
|
-
.toArray();
|
|
105
|
-
return result.map((doc) => this.parseRead(doc as WithId<T>));
|
|
106
|
-
} catch (err) {
|
|
107
|
-
return [];
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Get the most recent revision for a document.
|
|
113
|
-
* @returns Revision document or null if no revisions exist or if revision tracking is not enabled.
|
|
114
|
-
*/
|
|
115
|
-
async getMostRecentRevision(): Promise<WithId<T> | null> {
|
|
116
|
-
try {
|
|
117
|
-
if (!this.revision) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
const result = await this.revision
|
|
121
|
-
.find({}, { sort: { revision: -1 } })
|
|
122
|
-
.limit(1)
|
|
123
|
-
.toArray();
|
|
124
|
-
return result.length > 0 ? this.parseRead(result[0] as WithId<T>) : null;
|
|
125
|
-
} catch (err) {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Insert a document into the collection.
|
|
132
|
-
*
|
|
133
|
-
* If this collection has revision tracking enabled, it will also insert the document into the revision collection.
|
|
134
|
-
*
|
|
135
|
-
* @param doc - The document to insert into the collection.
|
|
136
|
-
*/
|
|
137
|
-
async insert(doc: T): Promise<void> {
|
|
138
|
-
try {
|
|
139
|
-
const parsed = this.parseWrite(doc);
|
|
140
|
-
await this.collection.insertOne(parsed as OptionalUnlessRequiredId<T>);
|
|
141
|
-
await this.trackRevision(parsed);
|
|
142
|
-
} catch (err) {
|
|
143
|
-
throw new Error(`Failed to insert document: ${(err as Error).message}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Update multiple documents by filter with a partial document.
|
|
149
|
-
* @param filter - The filter to find documents to update.
|
|
150
|
-
* @param update - The partial document to update.
|
|
151
|
-
*/
|
|
152
|
-
async update(filter: Filter<T>, update: Partial<T>): Promise<void> {
|
|
153
|
-
try {
|
|
154
|
-
const existing = await this.collection.find(filter).toArray();
|
|
155
|
-
if (!existing.length) return;
|
|
156
|
-
|
|
157
|
-
await this.collection.updateMany(filter, { $set: update });
|
|
158
|
-
|
|
159
|
-
// record updated versions in revisions
|
|
160
|
-
if (this.revision) {
|
|
161
|
-
const updatedDocs = existing.map((doc) => ({
|
|
162
|
-
...doc,
|
|
163
|
-
...update,
|
|
164
|
-
_id: new ObjectId(),
|
|
165
|
-
}));
|
|
166
|
-
await this.revision.insertMany(
|
|
167
|
-
updatedDocs as OptionalUnlessRequiredId<T>[],
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
} catch (err) {
|
|
171
|
-
throw new Error(`Failed to update documents: ${(err as Error).message}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Update a single document by filter with a partial document. This will automatically set the `updatedAt` field to the current date if it exists in the schema, and increment the `revision` field by 1 if it exists in the schema. It will also insert the updated document into the revision collection if revision tracking is enabled.
|
|
177
|
-
* @param filter - The filter to find the document to update.
|
|
178
|
-
* @param update - The partial document to update.
|
|
179
|
-
*/
|
|
180
|
-
async updateOne(filter: Filter<T>, update: Partial<T>): Promise<void> {
|
|
181
|
-
try {
|
|
182
|
-
const incomingUpdate = this.hasProperty("updatedAt")
|
|
183
|
-
? { ...update, updatedAt: new Date() }
|
|
184
|
-
: update;
|
|
185
|
-
const updateDoc = {
|
|
186
|
-
$set: incomingUpdate,
|
|
187
|
-
...(this.revision ? { $inc: { revision: 1 } } : {}),
|
|
188
|
-
} as UpdateFilter<T>;
|
|
189
|
-
|
|
190
|
-
await this.collection.updateOne(filter, updateDoc);
|
|
191
|
-
if (this.revision) {
|
|
192
|
-
await this.collection
|
|
193
|
-
.aggregate([
|
|
194
|
-
{
|
|
195
|
-
$match: filter,
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
$set: { ...update, _id: new ObjectId() },
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
$merge: {
|
|
202
|
-
into: {
|
|
203
|
-
db: this.collection.dbName,
|
|
204
|
-
col: this.revision.collectionName,
|
|
205
|
-
on: "_id",
|
|
206
|
-
whenNotMatched: "insert",
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
])
|
|
211
|
-
.toArray();
|
|
212
|
-
}
|
|
213
|
-
} catch (err) {
|
|
214
|
-
console.error("Failed to update document:", err);
|
|
215
|
-
throw new Error(`Failed to update document: ${(err as Error).message}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Soft delete a document by setting the `deletedAt` field to the current date.
|
|
221
|
-
* If the schema does not have a `deletedAt` field, it will perform a hard delete.
|
|
222
|
-
* @param filter - The filter to find documents to delete.
|
|
223
|
-
*/
|
|
224
|
-
async delete(filter: Filter<T>): Promise<void> {
|
|
225
|
-
try {
|
|
226
|
-
if (this.hasProperty("deletedAt")) {
|
|
227
|
-
const deletedAt = new Date();
|
|
228
|
-
const update = {
|
|
229
|
-
$set: {
|
|
230
|
-
deletedAt,
|
|
231
|
-
},
|
|
232
|
-
} as unknown as UpdateFilter<T>;
|
|
233
|
-
await this.collection.updateMany(filter, update);
|
|
234
|
-
if (this.revision) {
|
|
235
|
-
await this.collection
|
|
236
|
-
.aggregate([
|
|
237
|
-
{
|
|
238
|
-
$match: filter,
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
$set: { _id: new ObjectId(), deletedAt },
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
$merge: {
|
|
245
|
-
into: {
|
|
246
|
-
db: this.collection.dbName,
|
|
247
|
-
col: this.revision.collectionName,
|
|
248
|
-
on: "_id",
|
|
249
|
-
whenNotMatched: "insert",
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
])
|
|
254
|
-
.toArray();
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
await this.collection.deleteMany(filter);
|
|
258
|
-
}
|
|
259
|
-
} catch (err) {
|
|
260
|
-
throw new Error(`Failed to delete documents: ${(err as Error).message}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Checks if the schema has a given property.
|
|
266
|
-
* @param schema - The Zod schema to check against.
|
|
267
|
-
* @param property - The property to check for in the schema.
|
|
268
|
-
* @returns
|
|
269
|
-
*/
|
|
270
|
-
private hasProperty(property: string): boolean {
|
|
271
|
-
if (!this.schema) {
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const schemaLike = this.schema as SchemaParser<T>;
|
|
276
|
-
const shapeSource =
|
|
277
|
-
schemaLike.shape ??
|
|
278
|
-
(typeof schemaLike._def?.shape === "function"
|
|
279
|
-
? schemaLike._def.shape()
|
|
280
|
-
: schemaLike._def?.shape);
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
shapeSource !== null &&
|
|
284
|
-
typeof shapeSource === "object" &&
|
|
285
|
-
!Array.isArray(shapeSource)
|
|
286
|
-
) {
|
|
287
|
-
return property in shapeSource;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
}
|