@solidxai/core 0.1.8-beta.1 → 0.1.8-beta.10

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 (157) hide show
  1. package/dist/controllers/authentication.controller.d.ts +32 -2
  2. package/dist/controllers/authentication.controller.d.ts.map +1 -1
  3. package/dist/controllers/authentication.controller.js +80 -3
  4. package/dist/controllers/authentication.controller.js.map +1 -1
  5. package/dist/dtos/create-api-key.dto.d.ts +5 -0
  6. package/dist/dtos/create-api-key.dto.d.ts.map +1 -0
  7. package/dist/dtos/create-api-key.dto.js +34 -0
  8. package/dist/dtos/create-api-key.dto.js.map +1 -0
  9. package/dist/dtos/post-chatter-message.dto.d.ts +1 -0
  10. package/dist/dtos/post-chatter-message.dto.d.ts.map +1 -1
  11. package/dist/dtos/post-chatter-message.dto.js +6 -1
  12. package/dist/dtos/post-chatter-message.dto.js.map +1 -1
  13. package/dist/dtos/register-private.dto.d.ts +3 -5
  14. package/dist/dtos/register-private.dto.d.ts.map +1 -1
  15. package/dist/dtos/register-private.dto.js +6 -18
  16. package/dist/dtos/register-private.dto.js.map +1 -1
  17. package/dist/dtos/sso-exchange.dto.d.ts +4 -0
  18. package/dist/dtos/sso-exchange.dto.d.ts.map +1 -0
  19. package/dist/dtos/sso-exchange.dto.js +26 -0
  20. package/dist/dtos/sso-exchange.dto.js.map +1 -0
  21. package/dist/dtos/update-api-key.dto.d.ts +4 -0
  22. package/dist/dtos/update-api-key.dto.d.ts.map +1 -0
  23. package/dist/dtos/update-api-key.dto.js +28 -0
  24. package/dist/dtos/update-api-key.dto.js.map +1 -0
  25. package/dist/entities/agent-event.entity.d.ts +3 -12
  26. package/dist/entities/agent-event.entity.d.ts.map +1 -1
  27. package/dist/entities/agent-event.entity.js +21 -46
  28. package/dist/entities/agent-event.entity.js.map +1 -1
  29. package/dist/entities/agent-session.entity.d.ts +2 -11
  30. package/dist/entities/agent-session.entity.d.ts.map +1 -1
  31. package/dist/entities/agent-session.entity.js +15 -40
  32. package/dist/entities/agent-session.entity.js.map +1 -1
  33. package/dist/entities/field-metadata.entity.js +1 -1
  34. package/dist/entities/field-metadata.entity.js.map +1 -1
  35. package/dist/entities/legacy-common.entity.d.ts +9 -9
  36. package/dist/entities/legacy-common.entity.d.ts.map +1 -1
  37. package/dist/entities/legacy-common.entity.js +7 -7
  38. package/dist/entities/legacy-common.entity.js.map +1 -1
  39. package/dist/entities/sms-template.entity.d.ts.map +1 -1
  40. package/dist/entities/sms-template.entity.js +2 -1
  41. package/dist/entities/sms-template.entity.js.map +1 -1
  42. package/dist/entities/user-api-key.entity.d.ts +12 -0
  43. package/dist/entities/user-api-key.entity.d.ts.map +1 -0
  44. package/dist/entities/user-api-key.entity.js +62 -0
  45. package/dist/entities/user-api-key.entity.js.map +1 -0
  46. package/dist/entities/user.entity.d.ts +3 -0
  47. package/dist/entities/user.entity.d.ts.map +1 -1
  48. package/dist/entities/user.entity.js +12 -1
  49. package/dist/entities/user.entity.js.map +1 -1
  50. package/dist/enums/auth-type.enum.d.ts +2 -1
  51. package/dist/enums/auth-type.enum.d.ts.map +1 -1
  52. package/dist/enums/auth-type.enum.js +2 -1
  53. package/dist/enums/auth-type.enum.js.map +1 -1
  54. package/dist/guards/api-key.guard.d.ts +11 -0
  55. package/dist/guards/api-key.guard.d.ts.map +1 -0
  56. package/dist/guards/api-key.guard.js +43 -0
  57. package/dist/guards/api-key.guard.js.map +1 -0
  58. package/dist/guards/authentication.guard.d.ts +4 -2
  59. package/dist/guards/authentication.guard.d.ts.map +1 -1
  60. package/dist/guards/authentication.guard.js +7 -3
  61. package/dist/guards/authentication.guard.js.map +1 -1
  62. package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
  63. package/dist/helpers/bootstrap.helper.js +12 -1
  64. package/dist/helpers/bootstrap.helper.js.map +1 -1
  65. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.d.ts.map +1 -1
  66. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.js +15 -6
  67. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.js.map +1 -1
  68. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  69. package/dist/helpers/typeorm-db-helper.js +9 -0
  70. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +1 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/repository/user-api-key.repository.d.ts +12 -0
  76. package/dist/repository/user-api-key.repository.d.ts.map +1 -0
  77. package/dist/repository/user-api-key.repository.js +34 -0
  78. package/dist/repository/user-api-key.repository.js.map +1 -0
  79. package/dist/seeders/module-test-data.service.d.ts +5 -0
  80. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  81. package/dist/seeders/module-test-data.service.js +131 -4
  82. package/dist/seeders/module-test-data.service.js.map +1 -1
  83. package/dist/seeders/seed-data/solid-core-metadata.json +287 -197
  84. package/dist/services/api-key.service.d.ts +20 -0
  85. package/dist/services/api-key.service.d.ts.map +1 -0
  86. package/dist/services/api-key.service.js +98 -0
  87. package/dist/services/api-key.service.js.map +1 -0
  88. package/dist/services/authentication.service.d.ts +19 -1
  89. package/dist/services/authentication.service.d.ts.map +1 -1
  90. package/dist/services/authentication.service.js +31 -5
  91. package/dist/services/authentication.service.js.map +1 -1
  92. package/dist/services/chatter-message.service.d.ts.map +1 -1
  93. package/dist/services/chatter-message.service.js +6 -0
  94. package/dist/services/chatter-message.service.js.map +1 -1
  95. package/dist/services/export-transaction.service.d.ts.map +1 -1
  96. package/dist/services/export-transaction.service.js +0 -23
  97. package/dist/services/export-transaction.service.js.map +1 -1
  98. package/dist/services/field-metadata.service.d.ts +1 -3
  99. package/dist/services/field-metadata.service.d.ts.map +1 -1
  100. package/dist/services/field-metadata.service.js +6 -13
  101. package/dist/services/field-metadata.service.js.map +1 -1
  102. package/dist/services/file/disk-file.service.d.ts +1 -0
  103. package/dist/services/file/disk-file.service.d.ts.map +1 -1
  104. package/dist/services/file/disk-file.service.js +11 -3
  105. package/dist/services/file/disk-file.service.js.map +1 -1
  106. package/dist/services/media.service.d.ts +0 -1
  107. package/dist/services/media.service.d.ts.map +1 -1
  108. package/dist/services/media.service.js +10 -11
  109. package/dist/services/media.service.js.map +1 -1
  110. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  111. package/dist/services/settings/default-settings-provider.service.js +5 -2
  112. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  113. package/dist/services/sso-code-storage.service.d.ts +15 -0
  114. package/dist/services/sso-code-storage.service.d.ts.map +1 -0
  115. package/dist/services/sso-code-storage.service.js +47 -0
  116. package/dist/services/sso-code-storage.service.js.map +1 -0
  117. package/dist/services/user.service.d.ts.map +1 -1
  118. package/dist/services/user.service.js +3 -2
  119. package/dist/services/user.service.js.map +1 -1
  120. package/dist/solid-core.module.d.ts.map +1 -1
  121. package/dist/solid-core.module.js +10 -0
  122. package/dist/solid-core.module.js.map +1 -1
  123. package/package.json +1 -1
  124. package/src/controllers/authentication.controller.ts +59 -3
  125. package/src/dtos/create-api-key.dto.ts +14 -0
  126. package/src/dtos/post-chatter-message.dto.ts +4 -0
  127. package/src/dtos/register-private.dto.ts +5 -14
  128. package/src/dtos/sso-exchange.dto.ts +7 -0
  129. package/src/dtos/update-api-key.dto.ts +9 -0
  130. package/src/entities/agent-event.entity.ts +21 -55
  131. package/src/entities/agent-session.entity.ts +15 -47
  132. package/src/entities/field-metadata.entity.ts +1 -1
  133. package/src/entities/legacy-common.entity.ts +15 -15
  134. package/src/entities/sms-template.entity.ts +3 -2
  135. package/src/entities/user-api-key.entity.ts +37 -0
  136. package/src/entities/user.entity.ts +8 -0
  137. package/src/enums/auth-type.enum.ts +1 -0
  138. package/src/guards/api-key.guard.ts +32 -0
  139. package/src/guards/authentication.guard.ts +6 -3
  140. package/src/helpers/bootstrap.helper.ts +16 -1
  141. package/src/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.ts +17 -6
  142. package/src/helpers/typeorm-db-helper.ts +11 -0
  143. package/src/index.ts +1 -0
  144. package/src/repository/user-api-key.repository.ts +17 -0
  145. package/src/seeders/module-test-data.service.ts +165 -6
  146. package/src/seeders/seed-data/solid-core-metadata.json +287 -197
  147. package/src/services/api-key.service.ts +111 -0
  148. package/src/services/authentication.service.ts +35 -3
  149. package/src/services/chatter-message.service.ts +7 -0
  150. package/src/services/export-transaction.service.ts +0 -26
  151. package/src/services/field-metadata.service.ts +5 -12
  152. package/src/services/file/disk-file.service.ts +15 -7
  153. package/src/services/media.service.ts +12 -51
  154. package/src/services/settings/default-settings-provider.service.ts +5 -2
  155. package/src/services/sso-code-storage.service.ts +36 -0
  156. package/src/services/user.service.ts +3 -2
  157. package/src/solid-core.module.ts +10 -0
@@ -1,71 +1,39 @@
1
- import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
2
- import { Expose, Exclude } from 'class-transformer';
1
+ import { Column, Entity, Index } from 'typeorm';
2
+ import { CommonEntity } from 'src/entities/common.entity';
3
+ import { getColumnType } from 'src/helpers/typeorm-db-helper';
3
4
 
4
- @Exclude()
5
- @Entity({ name: 'ss_agent_sessions', synchronize: false })
6
- export class AgentSession {
7
- @Expose()
8
- @PrimaryGeneratedColumn({ type: 'integer' })
9
- id: number;
10
-
11
- @Expose()
5
+ @Entity({ name: 'ss_agent_sessions' })
6
+ export class AgentSession extends CommonEntity {
12
7
  @Index({ unique: true })
13
- @Column({ type: 'varchar', length: 36, name: 'session_id' })
8
+ @Column({ })
14
9
  sessionId: string;
15
10
 
16
- @Expose()
17
11
  @Index()
18
- @Column({ type: 'integer', nullable: true, name: 'user_id' })
12
+ @Column({ nullable: true })
19
13
  userId: number;
20
14
 
21
- @Expose()
22
- @Column({ type: 'text', nullable: true, name: 'project_root' })
15
+ @Column({ nullable: true, ...getColumnType('longText') })
23
16
  projectRoot: string;
24
17
 
25
- @Expose()
26
- @Column({ type: 'varchar', length: 255, name: 'model_name' })
18
+ @Column({ })
27
19
  modelName: string;
28
20
 
29
- @Expose()
30
21
  @Index()
31
- @Column({ type: 'varchar', length: 32, name: 'status' })
22
+ @Column({ })
32
23
  status: string;
33
24
 
34
- @Expose()
35
- @Column({ type: 'double precision', name: 'total_cost', default: 0 })
25
+ @Column({ default: 0, ...getColumnType('decimal') })
36
26
  totalCost: number;
37
27
 
38
- @Expose()
39
- @Column({ type: 'integer', name: 'total_steps', default: 0 })
28
+ @Column({ default: 0 })
40
29
  totalSteps: number;
41
30
 
42
- @Expose()
43
- @Column({ type: 'integer', name: 'total_input_tokens', default: 0 })
31
+ @Column({ default: 0 })
44
32
  totalInputTokens: number;
45
33
 
46
- @Expose()
47
- @Column({ type: 'integer', name: 'total_output_tokens', default: 0 })
34
+ @Column({ default: 0 })
48
35
  totalOutputTokens: number;
49
36
 
50
- @Expose()
51
- @Column({ type: 'text', nullable: true, name: 'summary' })
37
+ @Column({ nullable: true, ...getColumnType('longText') })
52
38
  summary: string;
53
-
54
- @Expose()
55
- @Column({ type: 'timestamp without time zone', name: 'created_at' })
56
- createdAt: Date;
57
-
58
- @Expose()
59
- @Column({ type: 'timestamp without time zone', name: 'updated_at' })
60
- updatedAt: Date;
61
-
62
- // The following properties satisfy CRUDService<T extends CommonEntity> structural typing
63
- // They are not mapped to DB columns (synchronize: false ensures no schema changes)
64
- deletedAt: Date;
65
- deletedTracker: string;
66
- publishedAt: Date;
67
- localeName: string;
68
- defaultEntityLocaleId: number;
69
- createdBy: number;
70
- updatedBy: number;
71
39
  }
@@ -119,7 +119,7 @@ export class FieldMetadata extends CommonEntity {
119
119
  @Column({ name: 'selection_static_values', nullable: true, type: 'simple-array' })
120
120
  selectionStaticValues: string[];
121
121
 
122
- @Column({ name: 'selection_value_type', nullable: true })
122
+ @Column({ name: 'selection_value_type', nullable: true, default: 'string' })
123
123
  selectionValueType: string = 'string';
124
124
 
125
125
  // @Column({ name: "computed", default: false })
@@ -11,30 +11,30 @@ export abstract class LegacyCommonEntity {
11
11
  // @Generated("increment")
12
12
  // id: number
13
13
 
14
- @CreateDateColumn({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_created_at`, transformer: LocalDateTimeTransformer })
15
- createdAt: Date;
14
+ @CreateDateColumn({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_created_at`, transformer: LocalDateTimeTransformer, nullable: true })
15
+ createdAt: Date | null;
16
16
 
17
- @UpdateDateColumn({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_updated_at`, transformer: LocalDateTimeTransformer })
18
- updatedAt: Date;
17
+ @UpdateDateColumn({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_updated_at`, transformer: LocalDateTimeTransformer, nullable: true })
18
+ updatedAt: Date | null;
19
19
 
20
- @DeleteDateColumn({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_deleted_at`, transformer: LocalDateTimeTransformer })
20
+ @DeleteDateColumn({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_deleted_at`, transformer: LocalDateTimeTransformer, nullable: true })
21
21
  @Index()
22
- deletedAt: Date;
22
+ deletedAt: Date | null;
23
23
 
24
- @Column({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_deleted_tracker`, default: "not-deleted" })
25
- deletedTracker: string;
24
+ @Column({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_deleted_tracker`, default: "not-deleted", nullable: true })
25
+ deletedTracker: string | null;
26
26
 
27
27
  @Expose()
28
28
  @Column({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_published_at`, default: null, nullable: true, transformer: LocalDateTimeTransformer })
29
- publishedAt: Date;
29
+ publishedAt: Date | null;
30
30
 
31
31
  @Expose()
32
- @Column({ type: "varchar", name: `${LEGACY_TABLE_FIELDS_PREFIX}_locale_name`, default: null })
33
- localeName: string;
32
+ @Column({ type: "varchar", name: `${LEGACY_TABLE_FIELDS_PREFIX}_locale_name`, default: null, nullable: true })
33
+ localeName: string | null;
34
34
 
35
35
  @Expose()
36
- @Column({ type: "int", name: `${LEGACY_TABLE_FIELDS_PREFIX}_default_entity_locale_id`, default: null })
37
- defaultEntityLocaleId: number;
36
+ @Column({ type: "int", name: `${LEGACY_TABLE_FIELDS_PREFIX}_default_entity_locale_id`, default: null, nullable: true })
37
+ defaultEntityLocaleId: number | null;
38
38
 
39
39
  // @Expose()
40
40
  // @Type( () => require('./user.entity').User?.default ?? require('./user.entity').User )
@@ -50,9 +50,9 @@ export abstract class LegacyCommonEntity {
50
50
 
51
51
  @Expose()
52
52
  @Column({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_created_by_id`, nullable: true })
53
- createdBy: number;
53
+ createdBy: number | null;
54
54
 
55
55
  @Expose()
56
56
  @Column({ name: `${LEGACY_TABLE_FIELDS_PREFIX}_updated_by_id`, nullable: true })
57
- updatedBy: number;
57
+ updatedBy: number | null;
58
58
  }
@@ -1,14 +1,15 @@
1
1
  import { CommonEntity } from 'src/entities/common.entity';
2
2
  import { Column, Entity, Index } from 'typeorm';
3
+ import { getColumnType } from 'src/helpers/typeorm-db-helper';
3
4
 
4
5
  @Entity("ss_sms_template")
5
6
  export class SmsTemplate extends CommonEntity {
6
7
  @Index({ unique: true })
7
- @Column({ name: "name", type: "varchar"})
8
+ @Column({ name: "name", type: "varchar" })
8
9
  name: string;
9
10
  @Column({ name: "display_name", type: "varchar" })
10
11
  displayName: string;
11
- @Column({ name: "body", type: "varchar", nullable: true })
12
+ @Column({ name: "body", ...getColumnType('longText'), nullable: true })
12
13
  body: string;
13
14
  @Column({ type: "varchar", nullable: true })
14
15
  smsProviderTemplateId: string;
@@ -0,0 +1,37 @@
1
+ import { Exclude, Expose } from "class-transformer";
2
+ import { CommonEntity } from "src/entities/common.entity";
3
+ import { Column, Entity, Index, ManyToOne } from "typeorm";
4
+ import { User } from "./user.entity";
5
+
6
+ @Entity("ss_user_api_key")
7
+ @Exclude()
8
+ export class UserApiKey extends CommonEntity {
9
+
10
+ @Expose()
11
+ @Column()
12
+ name: string;
13
+
14
+ // SHA-256 hash of the raw key — never exposed, same treatment as User.password
15
+ @Index({ unique: true })
16
+ @Column()
17
+ hashedKey: string;
18
+
19
+ @Expose()
20
+ @Column()
21
+ maskedKey: string;
22
+
23
+ @Expose()
24
+ @Column({ default: true })
25
+ isActive: boolean;
26
+
27
+ @Expose()
28
+ @Column({ nullable: true })
29
+ expiresAt: Date;
30
+
31
+ @Expose()
32
+ @Column({ nullable: true })
33
+ lastUsedAt: Date;
34
+
35
+ @ManyToOne(() => User, user => user.apiKeys)
36
+ user: User;
37
+ }
@@ -2,6 +2,7 @@ import { CommonEntity } from "src/entities/common.entity"
2
2
  import { Entity, Column, Index, JoinTable, ManyToMany, OneToMany, TableInheritance } from "typeorm";
3
3
  import { RoleMetadata } from 'src/entities/role-metadata.entity';
4
4
  import { UserViewMetadata } from 'src/entities/user-view-metadata.entity'
5
+ import { UserApiKey } from 'src/entities/user-api-key.entity'
5
6
  import { Exclude, Expose } from "class-transformer";
6
7
 
7
8
  @Entity("ss_user")
@@ -151,4 +152,11 @@ export class User extends CommonEntity {
151
152
  @Expose()
152
153
  _media: any;
153
154
 
155
+ @Column({ default: false })
156
+ @Expose()
157
+ isAllowedToGenerateApiKeys: boolean = false;
158
+
159
+ @OneToMany(() => UserApiKey, key => key.user)
160
+ apiKeys: UserApiKey[];
161
+
154
162
  }
@@ -1,4 +1,5 @@
1
1
  export enum AuthType {
2
2
  Bearer,
3
+ ApiKey,
3
4
  None,
4
5
  }
@@ -0,0 +1,32 @@
1
+ import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
2
+ import { Request } from 'express';
3
+ import { REQUEST_USER_KEY } from 'src/constants';
4
+ import { ApiKeyService } from 'src/services/api-key.service';
5
+ import { ClsService } from 'nestjs-cls';
6
+
7
+ @Injectable()
8
+ export class ApiKeyGuard implements CanActivate {
9
+ constructor(
10
+ private readonly apiKeyService: ApiKeyService,
11
+ private readonly cls: ClsService,
12
+ ) {}
13
+
14
+ async canActivate(context: ExecutionContext): Promise<boolean> {
15
+ const request = context.switchToHttp().getRequest<Request>();
16
+ const rawKey = this.extractKeyFromHeader(request);
17
+
18
+ if (!rawKey) {
19
+ throw new UnauthorizedException();
20
+ }
21
+
22
+ const activeUser = await this.apiKeyService.validate(rawKey);
23
+ request[REQUEST_USER_KEY] = activeUser;
24
+ this.cls.set(REQUEST_USER_KEY, activeUser);
25
+
26
+ return true;
27
+ }
28
+
29
+ private extractKeyFromHeader(request: Request): string | undefined {
30
+ return request.headers['solidx-api-key'] as string | undefined;
31
+ }
32
+ }
@@ -8,23 +8,26 @@ import { Reflector } from '@nestjs/core';
8
8
  import { AUTH_TYPE_KEY } from '../decorators/auth.decorator';
9
9
  import { AuthType } from '../enums/auth-type.enum';
10
10
  import { AccessTokenGuard } from './access-token.guard';
11
+ import { ApiKeyGuard } from './api-key.guard';
11
12
  import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
12
13
  import { PermissionMetadataService } from '../services/permission-metadata.service';
13
14
  import { ClsService } from 'nestjs-cls';
14
15
 
15
16
  @Injectable()
16
17
  export class AuthenticationGuard implements CanActivate {
17
- private static readonly defaultAuthType = AuthType.Bearer;
18
+ private static readonly defaultAuthTypes = [AuthType.Bearer, AuthType.ApiKey];
18
19
  private readonly authTypeGuardMap: Record<
19
20
  AuthType,
20
21
  CanActivate | CanActivate[]> = {
21
22
  [AuthType.Bearer]: this.accessTokenGuard,
23
+ [AuthType.ApiKey]: this.apiKeyGuard,
22
24
  [AuthType.None]: { canActivate: () => true },
23
25
  };
24
26
 
25
27
  constructor(
26
28
  private readonly reflector: Reflector,
27
29
  private readonly accessTokenGuard: AccessTokenGuard,
30
+ private readonly apiKeyGuard: ApiKeyGuard,
28
31
  private readonly permissionService: PermissionMetadataService,
29
32
  private readonly cls: ClsService,
30
33
  ) { }
@@ -49,7 +52,7 @@ export class AuthenticationGuard implements CanActivate {
49
52
  return true;
50
53
  }
51
54
 
52
- // TODO: Check if this permission viz. contextPermission is listed in the Public role.
55
+ // TODO: Check if this permission viz. contextPermission is listed in the Public role.
53
56
  const contextPermission = `${context.getClass().name}.${context.getHandler().name}`;
54
57
 
55
58
  const permissionExistsInRole = await this.permissionService.permissionExistsInRole('Public', contextPermission)
@@ -61,7 +64,7 @@ export class AuthenticationGuard implements CanActivate {
61
64
  const authTypes = this.reflector.getAllAndOverride<AuthType[]>(
62
65
  AUTH_TYPE_KEY,
63
66
  [context.getHandler(), context.getClass()],
64
- ) ?? [AuthenticationGuard.defaultAuthType];
67
+ ) ?? AuthenticationGuard.defaultAuthTypes;
65
68
  const guards = authTypes.map((type) => this.authTypeGuardMap[type]).flat();
66
69
  let error = new UnauthorizedException();
67
70
 
@@ -90,6 +90,21 @@ export async function bootstrapSolidApp(
90
90
  // Security headers
91
91
  app.use(helmet(buildDefaultSecurityHeaderOptions()));
92
92
 
93
+ // Nest's Swagger UI HTML injects inline styles; keep CSP strict elsewhere.
94
+ const isSwaggerPath = (path: string) =>
95
+ path === '/docs' ||
96
+ path === '/docs/' ||
97
+ path.startsWith('/docs/') ||
98
+ path === '/docs-json' ||
99
+ path === '/docs-yaml';
100
+
101
+ app.use((req: Request, res: Response, next: NextFunction) => {
102
+ if (isSwaggerPath(req.path)) {
103
+ res.removeHeader('Content-Security-Policy');
104
+ }
105
+ next();
106
+ });
107
+
93
108
  // Permissions-Policy header
94
109
  app.use((_req: Request, res: Response, next: NextFunction) => {
95
110
  res.setHeader('Permissions-Policy', buildPermissionsPolicyHeader(permissionsPolicyOverrides));
@@ -127,7 +142,7 @@ export async function bootstrapSolidApp(
127
142
 
128
143
  // Swagger
129
144
  if (swagger !== false) {
130
- const { title = 'Solid Starters', description = 'Solid Starters API', version = '1.0' } = swagger;
145
+ const { title = process.env.SOLID_APP_NAME, description = process.env.SOLID_APP_DESCRIPTION, version = '1.0' } = swagger;
131
146
  const swaggerConfig = new DocumentBuilder()
132
147
  .setTitle(title)
133
148
  .setDescription(description)
@@ -128,14 +128,25 @@ export class SelectionDynamicFieldCrudManager implements FieldCrudManager {
128
128
  }
129
129
 
130
130
  private providerInstance<T extends ISelectionProviderContext>(selectionDynamicProvider: string): ISelectionProvider<T> {
131
- const provider = this.options.discoveryService
132
- .getProviders()
133
- .filter((provider) => provider.name === selectionDynamicProvider)
134
- .pop();
135
- if (!provider) {
131
+ const providers = this.options.discoveryService.getProviders();
132
+
133
+ const byToken = providers.find((p) => p.name === selectionDynamicProvider);
134
+ if (byToken) {
135
+ return byToken.instance as ISelectionProvider<T>;
136
+ }
137
+
138
+ const byName = providers.find((p) => {
139
+ try {
140
+ return typeof p.instance?.name === 'function' && p.instance.name() === selectionDynamicProvider;
141
+ } catch {
142
+ return false;
143
+ }
144
+ });
145
+
146
+ if (!byName) {
136
147
  throw new Error(`Provider for ${selectionDynamicProvider} not found`);
137
148
  }
138
- return provider.instance as ISelectionProvider<T>;
149
+ return byName.instance as ISelectionProvider<T>;
139
150
  }
140
151
 
141
152
  private isApplyRequiredValidation(): boolean {
@@ -32,6 +32,14 @@ const SIMPLE_JSON_LARGE_TEXT_MAP: Record<DatasourceType, ColumnOptions> = {
32
32
  [DatasourceType.oracle]: { type: "clob" },
33
33
  };
34
34
 
35
+ const DECIMAL_MAP: Record<DatasourceType, ColumnOptions> = {
36
+ [DatasourceType.postgres]: { type: "float4" },
37
+ [DatasourceType.mssql]: { type: "float" },
38
+ [DatasourceType.mysql]: { type: "float" },
39
+ [DatasourceType.mariadb]: { type: "float" },
40
+ [DatasourceType.oracle]: { type: "float" },
41
+ };
42
+
35
43
  const solidCoreDbType: DatasourceType =
36
44
  Object.values(DatasourceType).includes(process.env.SOLID_CORE_DB_TYPE as DatasourceType)
37
45
  ? (process.env.SOLID_CORE_DB_TYPE as DatasourceType)
@@ -46,6 +54,9 @@ export function getColumnType(solidType: string): ColumnOptions {
46
54
  case "simpleJsonLargeText":
47
55
  return SIMPLE_JSON_LARGE_TEXT_MAP[solidCoreDbType];
48
56
 
57
+ case "decimal":
58
+ return DECIMAL_MAP[solidCoreDbType];
59
+
49
60
  default:
50
61
  return {};
51
62
  }
package/src/index.ts CHANGED
@@ -126,6 +126,7 @@ export * from './entities/permission-metadata.entity'
126
126
  export * from './entities/role-metadata.entity'
127
127
  export * from './entities/sms-template.entity'
128
128
  export * from './entities/user.entity'
129
+ export * from './entities/user-api-key.entity'
129
130
  export * from './entities/view-metadata.entity'
130
131
  export * from './entities/setting.entity'
131
132
  export * from './entities/saved-filters.entity'
@@ -0,0 +1,17 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { UserApiKey } from 'src/entities/user-api-key.entity';
3
+ import { RequestContextService } from 'src/services/request-context.service';
4
+ import { DataSource } from 'typeorm';
5
+ import { SecurityRuleRepository } from './security-rule.repository';
6
+ import { SolidBaseRepository } from './solid-base.repository';
7
+
8
+ @Injectable()
9
+ export class UserApiKeyRepository extends SolidBaseRepository<UserApiKey> {
10
+ constructor(
11
+ readonly dataSource: DataSource,
12
+ readonly requestContextService: RequestContextService,
13
+ readonly securityRuleRepository: SecurityRuleRepository,
14
+ ) {
15
+ super(UserApiKey, dataSource, requestContextService, securityRuleRepository);
16
+ }
17
+ }
@@ -2,15 +2,19 @@ import { Injectable, Logger } from '@nestjs/common';
2
2
  import { DiscoveryService, ModuleRef } from '@nestjs/core';
3
3
  import { getDataSourceToken } from '@nestjs/typeorm';
4
4
  import { classify } from '@angular-devkit/core/src/utils/strings';
5
- import { DataSource } from 'typeorm';
5
+ import { DataSource, EntityManager } from 'typeorm';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
 
9
9
  import solidCoreMetadata from './seed-data/solid-core-metadata.json';
10
10
  import { CreateModuleMetadataDto } from 'src/dtos/create-module-metadata.dto';
11
11
  import { CreateModelMetadataDto } from 'src/dtos/create-model-metadata.dto';
12
+ import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
12
13
  import { getDynamicModuleNamesBasedOnMetadata } from 'src/helpers/module.helper';
13
14
  import { SolidRegistry } from 'src/helpers/solid-registry';
15
+ import { MediaRepository } from 'src/repository/media.repository';
16
+ import { ModelMetadataService } from 'src/services/model-metadata.service';
17
+ import { getMediaStorageProvider } from 'src/services/mediaStorageProviders';
14
18
 
15
19
  @Injectable()
16
20
  export class ModuleTestDataService {
@@ -185,7 +189,7 @@ export class ModuleTestDataService {
185
189
  throw new Error('Module metadata missing from test data payload.');
186
190
  }
187
191
 
188
- // console.log(JSON.stringify(moduleMetadata, null, 2));
192
+ // console.log(JSON.stringify(moduleMetadata, null, 2));
189
193
 
190
194
  const testingData: Array<{ modelUserKey: string; data: Record<string, any> }> = overallMetadata?.testing?.data ?? [];
191
195
  if (testingData.length === 0) {
@@ -243,19 +247,174 @@ export class ModuleTestDataService {
243
247
  }
244
248
  }
245
249
 
250
+ // Strip media fields from entity payload — file paths cannot be saved as columns
251
+ const mediaPayload: Record<string, string> = {};
252
+ for (const field of modelDef.fields ?? []) {
253
+ if ((field.type === 'mediaSingle' || field.type === 'mediaMultiple') && payload[field.name] !== undefined) {
254
+ mediaPayload[field.name] = payload[field.name] as string;
255
+ delete payload[field.name];
256
+ }
257
+ }
258
+
259
+ // Strip many-to-many and one-to-many fields — these are resolved post-save via the relation builder
260
+ const multiRelationPayload: Array<{ field: any; userKeys: string[] }> = [];
261
+ for (const field of modelDef.fields ?? []) {
262
+ if (field.type !== 'relation') continue;
263
+ if (field.relationType !== 'many-to-many' && field.relationType !== 'one-to-many') continue;
264
+
265
+ const userKeysProp = `${field.name}UserKeys`;
266
+ if (userKeysProp in payload && Array.isArray(payload[userKeysProp])) {
267
+ multiRelationPayload.push({ field, userKeys: payload[userKeysProp] });
268
+ delete payload[userKeysProp];
269
+ }
270
+ // Remove raw field value if accidentally present
271
+ delete payload[field.name];
272
+ }
273
+
274
+ // Upsert entity, capturing the saved result for post-save steps
275
+ let savedEntity: any;
246
276
  const userKeyField = modelDef.userKeyFieldUserKey;
247
277
  if (userKeyField && payload[userKeyField] !== undefined) {
248
278
  const existing = await entityRepo.findOne({
249
279
  where: { [userKeyField]: payload[userKeyField] },
250
280
  });
251
281
  if (existing) {
252
- await entityRepo.save(entityRepo.merge(existing, payload));
253
- continue;
282
+ savedEntity = await entityRepo.save(entityRepo.merge(existing, payload));
283
+ } else {
284
+ savedEntity = await entityRepo.save(entityRepo.create(payload));
254
285
  }
286
+ } else {
287
+ savedEntity = await entityRepo.save(entityRepo.create(payload));
288
+ }
289
+
290
+ if (multiRelationPayload.length > 0) {
291
+ await this.seedMultiRelations(savedEntity.id, modelUserKey, multiRelationPayload, modelsByName);
292
+ }
293
+
294
+ if (Object.keys(mediaPayload).length > 0) {
295
+ await this.seedEntityMedia(savedEntity.id, modelUserKey, mediaPayload);
296
+ }
297
+ }
298
+ }
299
+
300
+ private async seedMultiRelations(
301
+ entityId: number,
302
+ modelUserKey: string,
303
+ relations: Array<{ field: any; userKeys: string[] }>,
304
+ modelsByName: Map<string, CreateModelMetadataDto>,
305
+ ): Promise<void> {
306
+ for (const { field, userKeys } of relations) {
307
+ if (!userKeys.length) continue;
308
+
309
+ const coModelName = field.relationCoModelSingularName;
310
+ const coModelDef = modelsByName.get(coModelName);
311
+ if (!coModelDef) {
312
+ throw new Error(`Relation model "${coModelName}" not found in metadata for field ${modelUserKey}.${field.name}`);
313
+ }
314
+ const coUserKeyField = coModelDef.userKeyFieldUserKey;
315
+ if (!coUserKeyField) {
316
+ throw new Error(`Relation model "${coModelName}" is missing userKeyFieldUserKey, needed to resolve ${modelUserKey}.${field.name}`);
317
+ }
318
+
319
+ const coRepo = this.resolveRepository(coModelName);
320
+ const resolvedIds: number[] = [];
321
+ for (const uk of userKeys) {
322
+ const related = typeof coRepo.findOneByUserKey === 'function'
323
+ ? await coRepo.findOneByUserKey(uk)
324
+ : await coRepo.findOne({ where: { [coUserKeyField]: uk } });
325
+ if (!related) {
326
+ throw new Error(`Related entity not found: ${coModelName}.${coUserKeyField}=${uk}`);
327
+ }
328
+ resolvedIds.push(related.id);
329
+ }
330
+
331
+ // Load currently associated entities to diff (set semantics — idempotent)
332
+ const existingRelated: any[] = await this.entityManager
333
+ .createQueryBuilder()
334
+ .relation(classify(modelUserKey), field.name)
335
+ .of(entityId)
336
+ .loadMany();
337
+ const existingIds: number[] = existingRelated.map((e) => e.id);
338
+
339
+ const toAdd = resolvedIds.filter((id) => !existingIds.includes(id));
340
+ const toRemove = existingIds.filter((id) => !resolvedIds.includes(id));
341
+
342
+ if (toAdd.length > 0 || toRemove.length > 0) {
343
+ await this.entityManager
344
+ .createQueryBuilder()
345
+ .relation(classify(modelUserKey), field.name)
346
+ .of(entityId)
347
+ .addAndRemove(toAdd, toRemove);
255
348
  }
256
349
 
257
- await entityRepo.save(entityRepo.create(payload));
350
+ this.logger.debug(`Seeded ${field.relationType} relation ${modelUserKey}.${field.name} entityId=${entityId}: +${toAdd.length} -${toRemove.length}`);
351
+ }
352
+ }
353
+
354
+ private async seedEntityMedia(
355
+ entityId: number,
356
+ modelUserKey: string,
357
+ mediaPayload: Record<string, string>,
358
+ ): Promise<void> {
359
+ const mediaBasePath = process.env.TEST_UPLOADS_MEDIA_FILE_PATH;
360
+ if (!mediaBasePath) {
361
+ throw new Error('TEST_UPLOADS_MEDIA_FILE_PATH is not set. Cannot seed test media.');
258
362
  }
363
+
364
+ const modelMetadata = await this.modelMetadataService.findOneBySingularName(modelUserKey, {
365
+ fields: {
366
+ model: { userKeyField: true },
367
+ mediaStorageProvider: true,
368
+ },
369
+ });
370
+
371
+ for (const [fieldName, fileName] of Object.entries(mediaPayload)) {
372
+ if (!fileName) continue;
373
+
374
+ const fieldMetadata = modelMetadata.fields.find((f) => f.name === fieldName);
375
+ if (!fieldMetadata) {
376
+ throw new Error(`Media field "${fieldName}" not found in loaded metadata for model ${modelUserKey}`);
377
+ }
378
+ if (!fieldMetadata.mediaStorageProvider) {
379
+ throw new Error(`Media field "${fieldName}" in model ${modelUserKey} has no storage provider configured`);
380
+ }
381
+
382
+ const storageProviderType = fieldMetadata.mediaStorageProvider.type as MediaStorageProviderType;
383
+ if (storageProviderType !== MediaStorageProviderType.Filesystem) {
384
+ throw new Error(`Test media seeding supports filesystem storage only. Field "${fieldName}" uses "${storageProviderType}".`);
385
+ }
386
+
387
+ // Idempotency: skip if media already exists for this entity + field
388
+ const existing = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(
389
+ entityId, fieldMetadata.id, fieldMetadata.model.id,
390
+ );
391
+ if (existing.length > 0) {
392
+ this.logger.debug(`Media already seeded for ${modelUserKey}.${fieldName} entityId=${entityId}, skipping`);
393
+ continue;
394
+ }
395
+
396
+ const sourcePath = path.join(mediaBasePath, fileName);
397
+ if (!fs.existsSync(sourcePath)) {
398
+ throw new Error(`Test media file not found: ${sourcePath}`);
399
+ }
400
+
401
+ const storageProvider = await getMediaStorageProvider(this.moduleRef, storageProviderType);
402
+ const stream = fs.createReadStream(sourcePath);
403
+ await storageProvider.storeStreams([[stream, fileName]], { id: entityId }, fieldMetadata);
404
+ this.logger.debug(`Seeded media for ${modelUserKey}.${fieldName} entityId=${entityId} file=${fileName}`);
405
+ }
406
+ }
407
+
408
+ private get entityManager(): EntityManager {
409
+ return this.moduleRef.get(EntityManager, { strict: false });
410
+ }
411
+
412
+ private get modelMetadataService(): ModelMetadataService {
413
+ return this.moduleRef.get(ModelMetadataService, { strict: false });
414
+ }
415
+
416
+ private get mediaRepository(): MediaRepository {
417
+ return this.moduleRef.get(MediaRepository, { strict: false });
259
418
  }
260
419
 
261
420
  private resolveRepository(modelUserKey: string): any {
@@ -308,7 +467,7 @@ export class ModuleTestDataService {
308
467
  if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
309
468
  return line;
310
469
  }
311
- const [rawKey, ...rest] = line.split('=');
470
+ const [rawKey] = line.split('=');
312
471
  const key = rawKey.trim();
313
472
  if (!key.endsWith('_DATABASE_NAME')) {
314
473
  return line;