create-forgeon 0.3.18 → 0.3.20
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 +1 -1
- package/src/cli/add-help.mjs +3 -2
- package/src/core/docs.test.mjs +1 -0
- package/src/core/scaffold.test.mjs +1 -0
- package/src/modules/accounts.mjs +28 -22
- package/src/modules/dependencies.mjs +153 -4
- package/src/modules/dependencies.test.mjs +58 -0
- package/src/modules/executor.test.mjs +73 -37
- package/src/modules/files-image.mjs +6 -4
- package/src/modules/files.mjs +5 -6
- package/src/modules/idempotency.test.mjs +3 -2
- package/src/modules/registry.mjs +20 -0
- package/src/modules/shared/files-runtime-wiring.mjs +13 -10
- package/src/run-add-module.mjs +39 -26
- package/src/run-add-module.test.mjs +76 -0
- package/src/run-scan-integrations.mjs +1 -0
- package/templates/base/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
- package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
- package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
- package/templates/module-presets/files/packages/files/package.json +1 -0
- package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
- package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
- package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
- package/templates/module-presets/files/packages/files/src/index.ts +1 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +0 -17
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
|
@@ -1,15 +1,43 @@
|
|
|
1
|
-
import { Prisma } from '@prisma/client';
|
|
2
|
-
import {
|
|
3
|
-
type AccountsPersistencePort,
|
|
4
|
-
type CreatePasswordAccountInput,
|
|
5
|
-
type PasswordAccountRecord,
|
|
6
|
-
type RefreshTokenRecord,
|
|
7
|
-
} from '@forgeon/accounts-api';
|
|
8
|
-
import { PrismaService } from '@forgeon/db-prisma';
|
|
9
1
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import type { IdentityProvider, JsonObject } from '@forgeon/accounts-contracts';
|
|
3
|
+
import { PrismaService } from '@forgeon/db-prisma';
|
|
4
|
+
import type { UserRecord } from './users.types';
|
|
5
|
+
import { mapUserRecord, toPrismaJsonInput } from './users.types';
|
|
6
|
+
|
|
7
|
+
export type PasswordAccountRecord = UserRecord & {
|
|
8
|
+
provider: IdentityProvider;
|
|
9
|
+
providerId: string;
|
|
10
|
+
passwordHash: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RefreshTokenRecord = {
|
|
14
|
+
id: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
tokenHash: string;
|
|
17
|
+
expiresAt: Date;
|
|
18
|
+
revokedAt: Date | null;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface CreatePasswordAccountInput {
|
|
23
|
+
email: string;
|
|
24
|
+
passwordHash: string;
|
|
25
|
+
status: string;
|
|
26
|
+
userData: JsonObject | null;
|
|
27
|
+
profile: {
|
|
28
|
+
name: string | null;
|
|
29
|
+
avatar: string | null;
|
|
30
|
+
data: JsonObject | null;
|
|
31
|
+
};
|
|
32
|
+
settings: {
|
|
33
|
+
theme: string | null;
|
|
34
|
+
locale: string | null;
|
|
35
|
+
data: JsonObject | null;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
10
38
|
|
|
11
39
|
@Injectable()
|
|
12
|
-
export class
|
|
40
|
+
export class AuthStore {
|
|
13
41
|
constructor(private readonly prisma: PrismaService) {}
|
|
14
42
|
|
|
15
43
|
async createPasswordAccount(input: CreatePasswordAccountInput): Promise<PasswordAccountRecord> {
|
|
@@ -17,19 +45,19 @@ export class PrismaAccountsPersistenceStore implements AccountsPersistencePort {
|
|
|
17
45
|
const user = await tx.user.create({
|
|
18
46
|
data: {
|
|
19
47
|
status: input.status,
|
|
20
|
-
data:
|
|
48
|
+
data: toPrismaJsonInput(input.userData),
|
|
21
49
|
profile: {
|
|
22
50
|
create: {
|
|
23
51
|
name: input.profile.name,
|
|
24
52
|
avatar: input.profile.avatar,
|
|
25
|
-
data:
|
|
53
|
+
data: toPrismaJsonInput(input.profile.data),
|
|
26
54
|
},
|
|
27
55
|
},
|
|
28
56
|
settings: {
|
|
29
57
|
create: {
|
|
30
58
|
theme: input.settings.theme,
|
|
31
59
|
locale: input.settings.locale,
|
|
32
|
-
data:
|
|
60
|
+
data: toPrismaJsonInput(input.settings.data),
|
|
33
61
|
},
|
|
34
62
|
},
|
|
35
63
|
},
|
|
@@ -159,174 +187,24 @@ export class PrismaAccountsPersistenceStore implements AccountsPersistencePort {
|
|
|
159
187
|
});
|
|
160
188
|
}
|
|
161
189
|
|
|
162
|
-
async findUserById(userId: string) {
|
|
163
|
-
const user = await this.prisma.user.findUnique({
|
|
164
|
-
where: { id: userId },
|
|
165
|
-
include: {
|
|
166
|
-
profile: true,
|
|
167
|
-
settings: true,
|
|
168
|
-
authIdentities: {
|
|
169
|
-
where: { provider: 'email' },
|
|
170
|
-
select: { providerId: true },
|
|
171
|
-
take: 1,
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return user ? this.mapUser(user) : null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async updateUser(input: { userId: string; data: Record<string, unknown> | null }) {
|
|
180
|
-
const user = await this.prisma.user.update({
|
|
181
|
-
where: { id: input.userId },
|
|
182
|
-
data: {
|
|
183
|
-
data: this.toNullableJson(input.data),
|
|
184
|
-
},
|
|
185
|
-
include: {
|
|
186
|
-
profile: true,
|
|
187
|
-
settings: true,
|
|
188
|
-
authIdentities: {
|
|
189
|
-
where: { provider: 'email' },
|
|
190
|
-
select: { providerId: true },
|
|
191
|
-
take: 1,
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return this.mapUser(user);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async updateUserProfile(input: {
|
|
200
|
-
userId: string;
|
|
201
|
-
name: string | null;
|
|
202
|
-
avatar: string | null;
|
|
203
|
-
data: Record<string, unknown> | null;
|
|
204
|
-
}) {
|
|
205
|
-
await this.prisma.userProfile.upsert({
|
|
206
|
-
where: { userId: input.userId },
|
|
207
|
-
create: {
|
|
208
|
-
userId: input.userId,
|
|
209
|
-
name: input.name,
|
|
210
|
-
avatar: input.avatar,
|
|
211
|
-
data: this.toNullableJson(input.data),
|
|
212
|
-
},
|
|
213
|
-
update: {
|
|
214
|
-
name: input.name,
|
|
215
|
-
avatar: input.avatar,
|
|
216
|
-
data: this.toNullableJson(input.data),
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const user = await this.findUserById(input.userId);
|
|
221
|
-
if (!user) {
|
|
222
|
-
throw new NotFoundException('User not found');
|
|
223
|
-
}
|
|
224
|
-
return user;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async updateUserSettings(input: {
|
|
228
|
-
userId: string;
|
|
229
|
-
theme: string | null;
|
|
230
|
-
locale: string | null;
|
|
231
|
-
data: Record<string, unknown> | null;
|
|
232
|
-
}) {
|
|
233
|
-
await this.prisma.userSettings.upsert({
|
|
234
|
-
where: { userId: input.userId },
|
|
235
|
-
create: {
|
|
236
|
-
userId: input.userId,
|
|
237
|
-
theme: input.theme,
|
|
238
|
-
locale: input.locale,
|
|
239
|
-
data: this.toNullableJson(input.data),
|
|
240
|
-
},
|
|
241
|
-
update: {
|
|
242
|
-
theme: input.theme,
|
|
243
|
-
locale: input.locale,
|
|
244
|
-
data: this.toNullableJson(input.data),
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const user = await this.findUserById(input.userId);
|
|
249
|
-
if (!user) {
|
|
250
|
-
throw new NotFoundException('User not found');
|
|
251
|
-
}
|
|
252
|
-
return user;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async softDeleteUser(userId: string, deletedAt: Date): Promise<void> {
|
|
256
|
-
await this.prisma.user.update({
|
|
257
|
-
where: { id: userId },
|
|
258
|
-
data: {
|
|
259
|
-
status: 'deleted',
|
|
260
|
-
deletedAt,
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
190
|
private mapPasswordAccount(user: {
|
|
266
191
|
id: string;
|
|
267
192
|
status: string;
|
|
268
|
-
data:
|
|
193
|
+
data: unknown;
|
|
269
194
|
createdAt: Date;
|
|
270
195
|
updatedAt: Date;
|
|
271
196
|
deletedAt: Date | null;
|
|
272
|
-
profile: { name: string | null; avatar: string | null; data:
|
|
273
|
-
settings: { theme: string | null; locale: string | null; data:
|
|
197
|
+
profile: { name: string | null; avatar: string | null; data: unknown } | null;
|
|
198
|
+
settings: { theme: string | null; locale: string | null; data: unknown } | null;
|
|
274
199
|
authCredential: { passwordHash: string } | null;
|
|
275
200
|
authIdentities: Array<{ provider?: string; providerId: string }>;
|
|
276
201
|
}): PasswordAccountRecord {
|
|
277
202
|
const emailIdentity = user.authIdentities[0];
|
|
278
203
|
return {
|
|
279
|
-
...
|
|
204
|
+
...mapUserRecord(user),
|
|
280
205
|
provider: 'email',
|
|
281
206
|
providerId: emailIdentity?.providerId ?? '',
|
|
282
207
|
passwordHash: user.authCredential?.passwordHash ?? null,
|
|
283
208
|
};
|
|
284
209
|
}
|
|
285
|
-
|
|
286
|
-
private mapUser(user: {
|
|
287
|
-
id: string;
|
|
288
|
-
status: string;
|
|
289
|
-
data: Prisma.JsonValue | null;
|
|
290
|
-
createdAt: Date;
|
|
291
|
-
updatedAt: Date;
|
|
292
|
-
deletedAt: Date | null;
|
|
293
|
-
profile: { name: string | null; avatar: string | null; data: Prisma.JsonValue | null } | null;
|
|
294
|
-
settings: { theme: string | null; locale: string | null; data: Prisma.JsonValue | null } | null;
|
|
295
|
-
authIdentities: Array<{ providerId: string }>;
|
|
296
|
-
}) {
|
|
297
|
-
return {
|
|
298
|
-
id: user.id,
|
|
299
|
-
email: user.authIdentities[0]?.providerId ?? null,
|
|
300
|
-
status: user.status,
|
|
301
|
-
data: this.fromJson(user.data),
|
|
302
|
-
createdAt: user.createdAt,
|
|
303
|
-
updatedAt: user.updatedAt,
|
|
304
|
-
deletedAt: user.deletedAt,
|
|
305
|
-
profile: user.profile
|
|
306
|
-
? {
|
|
307
|
-
name: user.profile.name,
|
|
308
|
-
avatar: user.profile.avatar,
|
|
309
|
-
data: this.fromJson(user.profile.data),
|
|
310
|
-
}
|
|
311
|
-
: null,
|
|
312
|
-
settings: user.settings
|
|
313
|
-
? {
|
|
314
|
-
theme: user.settings.theme,
|
|
315
|
-
locale: user.settings.locale,
|
|
316
|
-
data: this.fromJson(user.settings.data),
|
|
317
|
-
}
|
|
318
|
-
: null,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private toNullableJson(value: Record<string, unknown> | null) {
|
|
323
|
-
return value === null ? Prisma.JsonNull : (value as Prisma.InputJsonValue);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private fromJson(value: Prisma.JsonValue | null): Record<string, unknown> | null {
|
|
327
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
return value as Record<string, unknown>;
|
|
331
|
-
}
|
|
332
210
|
}
|
package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
2
|
DynamicModule,
|
|
3
3
|
Module,
|
|
4
4
|
ModuleMetadata,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
} from '@nestjs/common';
|
|
7
7
|
import { JwtModule } from '@nestjs/jwt';
|
|
8
8
|
import { PassportModule } from '@nestjs/passport';
|
|
9
|
+
import { DbPrismaModule } from '@forgeon/db-prisma';
|
|
9
10
|
import {
|
|
10
11
|
ACCOUNTS_AUTHZ_CLAIMS_RESOLVER,
|
|
11
12
|
NoopAccountsAuthzClaimsResolver,
|
|
@@ -17,12 +18,14 @@ import { AuthCoreService } from './auth-core.service';
|
|
|
17
18
|
import { AuthJwtService } from './auth-jwt.service';
|
|
18
19
|
import { AuthPasswordService } from './auth-password.service';
|
|
19
20
|
import { AuthService } from './auth.service';
|
|
21
|
+
import { AuthStore } from './auth.store';
|
|
20
22
|
import { JwtAuthGuard } from './access-token.guard';
|
|
21
23
|
import { JwtStrategy } from './jwt.strategy';
|
|
22
24
|
import { OwnerAccessGuard } from './owner-access.guard';
|
|
23
25
|
import { UsersController } from './users.controller';
|
|
24
26
|
import { UsersModule, USERS_MODULE_OPTIONS, type UsersModuleOptions } from './users-config';
|
|
25
27
|
import { UsersService } from './users.service';
|
|
28
|
+
import { UsersStore } from './users.store';
|
|
26
29
|
|
|
27
30
|
export interface ForgeonAccountsModuleOptions {
|
|
28
31
|
imports?: ModuleMetadata['imports'];
|
|
@@ -37,6 +40,7 @@ export class ForgeonAccountsModule {
|
|
|
37
40
|
module: ForgeonAccountsModule,
|
|
38
41
|
imports: [
|
|
39
42
|
AuthConfigModule,
|
|
43
|
+
DbPrismaModule,
|
|
40
44
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
|
41
45
|
JwtModule.register({}),
|
|
42
46
|
...(options.imports ?? []),
|
|
@@ -55,6 +59,8 @@ export class ForgeonAccountsModule {
|
|
|
55
59
|
provide: ACCOUNTS_AUTHZ_CLAIMS_RESOLVER,
|
|
56
60
|
useClass: NoopAccountsAuthzClaimsResolver,
|
|
57
61
|
},
|
|
62
|
+
AuthStore,
|
|
63
|
+
UsersStore,
|
|
58
64
|
AuthCoreService,
|
|
59
65
|
AuthJwtService,
|
|
60
66
|
AuthPasswordService,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './accounts-persistence.port';
|
|
1
|
+
export * from './accounts-email.port';
|
|
3
2
|
export * from './accounts-rbac.port';
|
|
4
3
|
export * from './auth-config.loader';
|
|
5
4
|
export * from './auth-config.module';
|
|
@@ -10,6 +9,7 @@ export * from './auth-env.schema';
|
|
|
10
9
|
export * from './auth-jwt.service';
|
|
11
10
|
export * from './auth-password.service';
|
|
12
11
|
export * from './auth.service';
|
|
12
|
+
export * from './auth.store';
|
|
13
13
|
export * from './auth.types';
|
|
14
14
|
export * from './dto';
|
|
15
15
|
export * from './forgeon-accounts.module';
|
|
@@ -19,6 +19,5 @@ export * from './owner-access.guard';
|
|
|
19
19
|
export * from './users-config';
|
|
20
20
|
export * from './users.controller';
|
|
21
21
|
export * from './users.service';
|
|
22
|
+
export * from './users.store';
|
|
22
23
|
export * from './users.types';
|
|
23
|
-
|
|
24
|
-
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
2
2
|
import type { UpdateUserProfileRequest, UpdateUserSettingsRequest, UpdateUserRequest } from '@forgeon/accounts-contracts';
|
|
3
|
-
import { ACCOUNTS_PERSISTENCE_PORT, type AccountsPersistencePort } from './accounts-persistence.port';
|
|
4
3
|
import { USERS_MODULE_OPTIONS, type UsersModuleOptions } from './users-config';
|
|
4
|
+
import { UsersStore } from './users.store';
|
|
5
5
|
import { mergeObjects, normalizeObject, toUserRecordDto } from './users.types';
|
|
6
6
|
|
|
7
7
|
@Injectable()
|
|
8
8
|
export class UsersService {
|
|
9
9
|
constructor(
|
|
10
|
-
|
|
11
|
-
private readonly persistence: AccountsPersistencePort,
|
|
10
|
+
private readonly usersStore: UsersStore,
|
|
12
11
|
@Inject(USERS_MODULE_OPTIONS)
|
|
13
12
|
private readonly usersModuleOptions: UsersModuleOptions,
|
|
14
13
|
) {}
|
|
15
14
|
|
|
16
15
|
async findById(userId: string) {
|
|
17
|
-
const user = await this.
|
|
16
|
+
const user = await this.usersStore.findById(userId);
|
|
18
17
|
return user ? toUserRecordDto(user) : null;
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -27,12 +26,12 @@ export class UsersService {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
async update(userId: string, input: UpdateUserRequest) {
|
|
30
|
-
const current = await this.
|
|
29
|
+
const current = await this.usersStore.findById(userId);
|
|
31
30
|
if (!current) {
|
|
32
31
|
throw new NotFoundException('User not found');
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
const updated = await this.
|
|
34
|
+
const updated = await this.usersStore.updateUser({
|
|
36
35
|
userId,
|
|
37
36
|
data: mergeObjects(current.data ?? this.usersModuleOptions.user, input.data),
|
|
38
37
|
});
|
|
@@ -40,12 +39,12 @@ export class UsersService {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
async updateProfile(userId: string, input: UpdateUserProfileRequest) {
|
|
43
|
-
const current = await this.
|
|
42
|
+
const current = await this.usersStore.findById(userId);
|
|
44
43
|
if (!current) {
|
|
45
44
|
throw new NotFoundException('User not found');
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
const updated = await this.
|
|
47
|
+
const updated = await this.usersStore.updateUserProfile({
|
|
49
48
|
userId,
|
|
50
49
|
name: input.name ?? current.profile?.name ?? null,
|
|
51
50
|
avatar: input.avatar ?? current.profile?.avatar ?? null,
|
|
@@ -55,12 +54,12 @@ export class UsersService {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
async updateSettings(userId: string, input: UpdateUserSettingsRequest) {
|
|
58
|
-
const current = await this.
|
|
57
|
+
const current = await this.usersStore.findById(userId);
|
|
59
58
|
if (!current) {
|
|
60
59
|
throw new NotFoundException('User not found');
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
const updated = await this.
|
|
62
|
+
const updated = await this.usersStore.updateUserSettings({
|
|
64
63
|
userId,
|
|
65
64
|
theme: input.theme ?? current.settings?.theme ?? null,
|
|
66
65
|
locale: input.locale ?? current.settings?.locale ?? null,
|
|
@@ -70,7 +69,7 @@ export class UsersService {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
async softDelete(userId: string): Promise<void> {
|
|
73
|
-
await this.
|
|
72
|
+
await this.usersStore.softDelete(userId, new Date());
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
resolveUserData(input: unknown) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import type { JsonObject } from '@forgeon/accounts-contracts';
|
|
3
|
+
import { PrismaService } from '@forgeon/db-prisma';
|
|
4
|
+
import type { UserRecord } from './users.types';
|
|
5
|
+
import { mapUserRecord, toPrismaJsonInput } from './users.types';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class UsersStore {
|
|
9
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
10
|
+
|
|
11
|
+
async findById(userId: string): Promise<UserRecord | null> {
|
|
12
|
+
const user = await this.prisma.user.findUnique({
|
|
13
|
+
where: { id: userId },
|
|
14
|
+
include: {
|
|
15
|
+
profile: true,
|
|
16
|
+
settings: true,
|
|
17
|
+
authIdentities: {
|
|
18
|
+
where: { provider: 'email' },
|
|
19
|
+
select: { providerId: true },
|
|
20
|
+
take: 1,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return user ? mapUserRecord(user) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async updateUser(input: { userId: string; data: JsonObject | null }): Promise<UserRecord> {
|
|
29
|
+
const user = await this.prisma.user.update({
|
|
30
|
+
where: { id: input.userId },
|
|
31
|
+
data: {
|
|
32
|
+
data: toPrismaJsonInput(input.data),
|
|
33
|
+
},
|
|
34
|
+
include: {
|
|
35
|
+
profile: true,
|
|
36
|
+
settings: true,
|
|
37
|
+
authIdentities: {
|
|
38
|
+
where: { provider: 'email' },
|
|
39
|
+
select: { providerId: true },
|
|
40
|
+
take: 1,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return mapUserRecord(user);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async updateUserProfile(input: {
|
|
49
|
+
userId: string;
|
|
50
|
+
name: string | null;
|
|
51
|
+
avatar: string | null;
|
|
52
|
+
data: JsonObject | null;
|
|
53
|
+
}): Promise<UserRecord> {
|
|
54
|
+
await this.prisma.userProfile.upsert({
|
|
55
|
+
where: { userId: input.userId },
|
|
56
|
+
create: {
|
|
57
|
+
userId: input.userId,
|
|
58
|
+
name: input.name,
|
|
59
|
+
avatar: input.avatar,
|
|
60
|
+
data: toPrismaJsonInput(input.data),
|
|
61
|
+
},
|
|
62
|
+
update: {
|
|
63
|
+
name: input.name,
|
|
64
|
+
avatar: input.avatar,
|
|
65
|
+
data: toPrismaJsonInput(input.data),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const user = await this.findById(input.userId);
|
|
70
|
+
if (!user) {
|
|
71
|
+
throw new NotFoundException('User not found');
|
|
72
|
+
}
|
|
73
|
+
return user;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async updateUserSettings(input: {
|
|
77
|
+
userId: string;
|
|
78
|
+
theme: string | null;
|
|
79
|
+
locale: string | null;
|
|
80
|
+
data: JsonObject | null;
|
|
81
|
+
}): Promise<UserRecord> {
|
|
82
|
+
await this.prisma.userSettings.upsert({
|
|
83
|
+
where: { userId: input.userId },
|
|
84
|
+
create: {
|
|
85
|
+
userId: input.userId,
|
|
86
|
+
theme: input.theme,
|
|
87
|
+
locale: input.locale,
|
|
88
|
+
data: toPrismaJsonInput(input.data),
|
|
89
|
+
},
|
|
90
|
+
update: {
|
|
91
|
+
theme: input.theme,
|
|
92
|
+
locale: input.locale,
|
|
93
|
+
data: toPrismaJsonInput(input.data),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const user = await this.findById(input.userId);
|
|
98
|
+
if (!user) {
|
|
99
|
+
throw new NotFoundException('User not found');
|
|
100
|
+
}
|
|
101
|
+
return user;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async softDelete(userId: string, deletedAt: Date): Promise<void> {
|
|
105
|
+
await this.prisma.user.update({
|
|
106
|
+
where: { id: userId },
|
|
107
|
+
data: {
|
|
108
|
+
status: 'deleted',
|
|
109
|
+
deletedAt,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -34,6 +34,54 @@ export function mergeObjects(baseValue: unknown, patchValue: unknown): JsonObjec
|
|
|
34
34
|
return Object.keys(merged).length > 0 ? merged : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function toPrismaJsonInput(value: JsonObject | null) {
|
|
38
|
+
return (value ?? undefined) as never;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mapUserRecord(source: {
|
|
42
|
+
id: string;
|
|
43
|
+
status: string;
|
|
44
|
+
data: unknown;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
updatedAt: Date;
|
|
47
|
+
deletedAt: Date | null;
|
|
48
|
+
profile?: {
|
|
49
|
+
name: string | null;
|
|
50
|
+
avatar: string | null;
|
|
51
|
+
data: unknown;
|
|
52
|
+
} | null;
|
|
53
|
+
settings?: {
|
|
54
|
+
theme: string | null;
|
|
55
|
+
locale: string | null;
|
|
56
|
+
data: unknown;
|
|
57
|
+
} | null;
|
|
58
|
+
authIdentities?: Array<{ providerId: string }>;
|
|
59
|
+
}): UserRecord {
|
|
60
|
+
return {
|
|
61
|
+
id: source.id,
|
|
62
|
+
email: source.authIdentities?.[0]?.providerId ?? null,
|
|
63
|
+
status: source.status,
|
|
64
|
+
data: normalizeObject(source.data),
|
|
65
|
+
createdAt: source.createdAt,
|
|
66
|
+
updatedAt: source.updatedAt,
|
|
67
|
+
deletedAt: source.deletedAt,
|
|
68
|
+
profile: source.profile
|
|
69
|
+
? {
|
|
70
|
+
name: source.profile.name,
|
|
71
|
+
avatar: source.profile.avatar,
|
|
72
|
+
data: normalizeObject(source.profile.data),
|
|
73
|
+
}
|
|
74
|
+
: null,
|
|
75
|
+
settings: source.settings
|
|
76
|
+
? {
|
|
77
|
+
theme: source.settings.theme,
|
|
78
|
+
locale: source.settings.locale,
|
|
79
|
+
data: normalizeObject(source.settings.data),
|
|
80
|
+
}
|
|
81
|
+
: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
37
85
|
export function toProfileDto(record: UserRecord['profile']): UserProfileDto {
|
|
38
86
|
return {
|
|
39
87
|
name: record?.name ?? null,
|
|
@@ -1,102 +1,7 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
|
-
import type { FileVariantKey } from './files.types';
|
|
3
2
|
|
|
4
|
-
export const FILES_PERSISTENCE_PORT = 'FORGEON_FILES_PERSISTENCE_PORT';
|
|
5
3
|
export const FILES_STORAGE_ADAPTER = 'FORGEON_FILES_STORAGE_ADAPTER';
|
|
6
4
|
|
|
7
|
-
export type FilesBlobRecord = {
|
|
8
|
-
id: string;
|
|
9
|
-
hash: string;
|
|
10
|
-
size: number;
|
|
11
|
-
mimeType: string;
|
|
12
|
-
storageDriver: string;
|
|
13
|
-
storageKey: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type FilesBlobRef = FilesBlobRecord & {
|
|
17
|
-
created: boolean;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type FilesRecordVariant = {
|
|
21
|
-
variantKey: string;
|
|
22
|
-
blobId: string;
|
|
23
|
-
mimeType: string;
|
|
24
|
-
size: number;
|
|
25
|
-
status: string;
|
|
26
|
-
blob?: {
|
|
27
|
-
storageDriver: string;
|
|
28
|
-
storageKey: string;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type FilesRecordAggregate = {
|
|
33
|
-
id: string;
|
|
34
|
-
publicId: string;
|
|
35
|
-
storageKey: string;
|
|
36
|
-
originalName: string;
|
|
37
|
-
mimeType: string;
|
|
38
|
-
size: number;
|
|
39
|
-
storageDriver: string;
|
|
40
|
-
ownerType: string;
|
|
41
|
-
ownerId: string | null;
|
|
42
|
-
visibility: string;
|
|
43
|
-
createdById: string | null;
|
|
44
|
-
createdAt: Date;
|
|
45
|
-
updatedAt: Date;
|
|
46
|
-
variants?: FilesRecordVariant[];
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export type FilesRecordCreateInput = {
|
|
50
|
-
publicId: string;
|
|
51
|
-
storageKey: string;
|
|
52
|
-
originalName: string;
|
|
53
|
-
mimeType: string;
|
|
54
|
-
size: number;
|
|
55
|
-
storageDriver: string;
|
|
56
|
-
ownerType: string;
|
|
57
|
-
ownerId: string | null;
|
|
58
|
-
visibility: string;
|
|
59
|
-
createdById: string | null;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export type FilesBlobCreateInput = {
|
|
63
|
-
hash: string;
|
|
64
|
-
size: number;
|
|
65
|
-
mimeType: string;
|
|
66
|
-
storageDriver: string;
|
|
67
|
-
storageKey: string;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export type FilesVariantCreateInput = {
|
|
71
|
-
fileId: string;
|
|
72
|
-
variantKey: FileVariantKey;
|
|
73
|
-
blobId: string;
|
|
74
|
-
mimeType: string;
|
|
75
|
-
size: number;
|
|
76
|
-
status: string;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export interface FilesPersistencePort {
|
|
80
|
-
createFileRecord(data: FilesRecordCreateInput): Promise<{
|
|
81
|
-
id: string;
|
|
82
|
-
publicId: string;
|
|
83
|
-
}>;
|
|
84
|
-
deleteFileRecordById(id: string): Promise<void>;
|
|
85
|
-
deleteFileRecordByPublicId(publicId: string): Promise<void>;
|
|
86
|
-
findFileRecordWithVariantKeys(publicId: string): Promise<FilesRecordAggregate | null>;
|
|
87
|
-
findFileRecordForDelete(publicId: string): Promise<FilesRecordAggregate | null>;
|
|
88
|
-
findFileRecordForDownload(publicId: string): Promise<FilesRecordAggregate | null>;
|
|
89
|
-
countOwnerUsage(ownerType: string, ownerId: string): Promise<{
|
|
90
|
-
filesCount: number;
|
|
91
|
-
totalBytes: number;
|
|
92
|
-
}>;
|
|
93
|
-
findBlobRef(hash: string, size: number, mimeType: string, storageDriver: string): Promise<FilesBlobRef | null>;
|
|
94
|
-
createBlob(data: FilesBlobCreateInput): Promise<FilesBlobRecord>;
|
|
95
|
-
createVariants(data: FilesVariantCreateInput[]): Promise<void>;
|
|
96
|
-
findBlobById(id: string): Promise<FilesBlobRecord | null>;
|
|
97
|
-
deleteBlobIfUnreferenced(id: string): Promise<boolean>;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
5
|
export interface FilesStorageAdapter {
|
|
101
6
|
readonly driver: string;
|
|
102
7
|
put(buffer: Buffer, fileName: string): Promise<{
|