create-forgeon 0.3.19 → 0.3.21

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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add-help.mjs +3 -2
  3. package/src/core/docs.test.mjs +1 -0
  4. package/src/core/scaffold.test.mjs +1 -0
  5. package/src/modules/accounts.mjs +9 -18
  6. package/src/modules/dependencies.mjs +153 -4
  7. package/src/modules/dependencies.test.mjs +58 -0
  8. package/src/modules/executor.test.mjs +544 -515
  9. package/src/modules/files-access.mjs +375 -375
  10. package/src/modules/files-image.mjs +512 -510
  11. package/src/modules/files-quotas.mjs +365 -365
  12. package/src/modules/files.mjs +5 -6
  13. package/src/modules/idempotency.test.mjs +3 -2
  14. package/src/modules/registry.mjs +20 -0
  15. package/src/modules/shared/files-runtime-wiring.mjs +13 -10
  16. package/src/run-add-module.mjs +39 -26
  17. package/src/run-add-module.test.mjs +228 -152
  18. package/src/run-scan-integrations.mjs +1 -0
  19. package/templates/base/package.json +1 -0
  20. package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
  21. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
  22. package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
  23. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
  24. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
  25. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
  26. package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
  27. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
  28. package/templates/module-presets/files/packages/files/package.json +1 -0
  29. package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
  30. package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
  31. package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
  32. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
  33. package/templates/module-presets/files/packages/files/src/index.ts +1 -0
  34. package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -20
  35. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -118
  36. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +18 -18
  37. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
  38. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
@@ -12,12 +12,9 @@ import type {
12
12
  UserRecordDto,
13
13
  } from '@forgeon/accounts-contracts';
14
14
  import { ACCOUNTS_EMAIL_PORT, type AccountsEmailPort } from './accounts-email.port';
15
- import {
16
- ACCOUNTS_PERSISTENCE_PORT,
17
- type AccountsPersistencePort,
18
- } from './accounts-persistence.port';
19
15
  import { AuthJwtService } from './auth-jwt.service';
20
16
  import { AuthPasswordService } from './auth-password.service';
17
+ import { AuthStore } from './auth.store';
21
18
  import type { AuthRefreshTokenPayload } from './auth.types';
22
19
  import { UsersService } from './users.service';
23
20
  import { toUserRecordDto } from './users.types';
@@ -33,8 +30,7 @@ const AUTH_ERROR_CODES = {
33
30
  @Injectable()
34
31
  export class AuthCoreService {
35
32
  constructor(
36
- @Inject(ACCOUNTS_PERSISTENCE_PORT)
37
- private readonly persistence: AccountsPersistencePort,
33
+ private readonly authStore: AuthStore,
38
34
  @Inject(ACCOUNTS_EMAIL_PORT)
39
35
  private readonly emailPort: AccountsEmailPort,
40
36
  private readonly authJwtService: AuthJwtService,
@@ -44,7 +40,7 @@ export class AuthCoreService {
44
40
 
45
41
  async registerWithPassword(input: RegisterRequest): Promise<AuthSessionResponse> {
46
42
  const email = input.email.trim().toLowerCase();
47
- const existing = await this.persistence.findPasswordAccountByEmail(email);
43
+ const existing = await this.authStore.findPasswordAccountByEmail(email);
48
44
  if (existing) {
49
45
  throw new ConflictException({
50
46
  message: 'Email is already registered',
@@ -53,7 +49,7 @@ export class AuthCoreService {
53
49
  }
54
50
 
55
51
  const passwordHash = await this.authPasswordService.hash(input.password);
56
- const account = await this.persistence.createPasswordAccount({
52
+ const account = await this.authStore.createPasswordAccount({
57
53
  email,
58
54
  passwordHash,
59
55
  status: 'active',
@@ -88,7 +84,7 @@ export class AuthCoreService {
88
84
 
89
85
  async loginWithPassword(emailInput: string, password: string): Promise<AuthSessionResponse> {
90
86
  const email = emailInput.trim().toLowerCase();
91
- const account = await this.persistence.findPasswordAccountByEmail(email);
87
+ const account = await this.authStore.findPasswordAccountByEmail(email);
92
88
  if (!account?.passwordHash) {
93
89
  throw this.invalidCredentialsError();
94
90
  }
@@ -119,7 +115,7 @@ export class AuthCoreService {
119
115
  });
120
116
  }
121
117
 
122
- const record = await this.persistence.findRefreshTokenById(payload.jti);
118
+ const record = await this.authStore.findRefreshTokenById(payload.jti);
123
119
  if (!record || record.revokedAt || record.userId !== payload.sub || record.expiresAt <= new Date()) {
124
120
  throw new UnauthorizedException({
125
121
  message: 'Refresh token is invalid or expired',
@@ -129,14 +125,14 @@ export class AuthCoreService {
129
125
 
130
126
  const matched = await this.authPasswordService.verify(refreshToken, record.tokenHash);
131
127
  if (!matched) {
132
- await this.persistence.revokeRefreshToken(record.id, new Date());
128
+ await this.authStore.revokeRefreshToken(record.id, new Date());
133
129
  throw new UnauthorizedException({
134
130
  message: 'Refresh token is invalid or expired',
135
131
  details: { code: AUTH_ERROR_CODES.refreshInvalid },
136
132
  });
137
133
  }
138
134
 
139
- const account = await this.persistence.findAccountByUserId(payload.sub);
135
+ const account = await this.authStore.findAccountByUserId(payload.sub);
140
136
  if (!account) {
141
137
  throw new UnauthorizedException({
142
138
  message: 'Refresh token is invalid or expired',
@@ -145,7 +141,7 @@ export class AuthCoreService {
145
141
  }
146
142
 
147
143
  this.assertAccountActive(account);
148
- await this.persistence.revokeRefreshToken(record.id, new Date());
144
+ await this.authStore.revokeRefreshToken(record.id, new Date());
149
145
  return this.issueSession(toUserRecordDto(account));
150
146
  }
151
147
 
@@ -158,18 +154,18 @@ export class AuthCoreService {
158
154
  });
159
155
  }
160
156
 
161
- await this.persistence.revokeRefreshToken(payload.jti, new Date());
157
+ await this.authStore.revokeRefreshToken(payload.jti, new Date());
162
158
  }
163
159
 
164
160
  async changePassword(userId: string, newPassword: string): Promise<void> {
165
161
  const passwordHash = await this.authPasswordService.hash(newPassword);
166
- await this.persistence.updatePassword(userId, passwordHash);
167
- await this.persistence.revokeRefreshTokensForUser(userId, new Date());
162
+ await this.authStore.updatePassword(userId, passwordHash);
163
+ await this.authStore.revokeRefreshTokensForUser(userId, new Date());
168
164
  }
169
165
 
170
166
  async requestPasswordReset(emailInput: string) {
171
167
  const email = emailInput.trim().toLowerCase();
172
- const account = await this.persistence.findPasswordAccountByEmail(email);
168
+ const account = await this.authStore.findPasswordAccountByEmail(email);
173
169
  if (account) {
174
170
  await this.emailPort.sendPasswordResetEmail({
175
171
  email,
@@ -218,7 +214,7 @@ export class AuthCoreService {
218
214
  return {
219
215
  status: 'ok',
220
216
  feature: 'accounts',
221
- storage: 'db-adapter',
217
+ storage: 'db-prisma',
222
218
  emailDelivery: 'stub',
223
219
  selfServiceRoutes: [
224
220
  '/api/users/:id',
@@ -245,7 +241,7 @@ export class AuthCoreService {
245
241
  ]);
246
242
 
247
243
  const tokenHash = await this.authPasswordService.hash(refreshToken);
248
- await this.persistence.createRefreshToken({
244
+ await this.authStore.createRefreshToken({
249
245
  id: refreshId,
250
246
  userId: user.id,
251
247
  tokenHash,
@@ -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 PrismaAccountsPersistenceStore implements AccountsPersistencePort {
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: this.toNullableJson(input.userData),
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: this.toNullableJson(input.profile.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: this.toNullableJson(input.settings.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: Prisma.JsonValue | null;
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: Prisma.JsonValue | null } | null;
273
- settings: { theme: string | null; locale: string | null; data: Prisma.JsonValue | null } | null;
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
- ...this.mapUser(user),
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
  }
@@ -1,4 +1,4 @@
1
- import {
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
- export * from './accounts-email.port';
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
- @Inject(ACCOUNTS_PERSISTENCE_PORT)
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.persistence.findUserById(userId);
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.persistence.findUserById(userId);
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.persistence.updateUser({
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.persistence.findUserById(userId);
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.persistence.updateUserProfile({
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.persistence.findUserById(userId);
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.persistence.updateUserSettings({
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.persistence.softDeleteUser(userId, new Date());
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,
@@ -8,6 +8,7 @@
8
8
  "build": "tsc -p tsconfig.json"
9
9
  },
10
10
  "dependencies": {
11
+ "@forgeon/db-prisma": "workspace:*",
11
12
  "@nestjs/common": "^11.0.1",
12
13
  "@nestjs/config": "^4.0.0",
13
14
  "@nestjs/platform-express": "^11.0.1",