@solidxai/core 0.1.13-beta.7 → 0.1.13-beta.8

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 (257) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/CHANGELOG.md +548 -0
  3. package/CLAUDE.md +26 -0
  4. package/dist/constants/draft-publish-fields.d.ts +2 -0
  5. package/dist/constants/draft-publish-fields.d.ts.map +1 -0
  6. package/dist/constants/draft-publish-fields.js +5 -0
  7. package/dist/constants/draft-publish-fields.js.map +1 -0
  8. package/dist/constants/media-file-types.d.ts +9 -0
  9. package/dist/constants/media-file-types.d.ts.map +1 -1
  10. package/dist/constants/media-file-types.js +27 -1
  11. package/dist/constants/media-file-types.js.map +1 -1
  12. package/dist/controllers/action-metadata.controller.d.ts +1 -1
  13. package/dist/controllers/action-metadata.controller.js +1 -1
  14. package/dist/controllers/agent-event.controller.d.ts +1 -1
  15. package/dist/controllers/agent-event.controller.js +1 -1
  16. package/dist/controllers/agent-session.controller.d.ts +1 -1
  17. package/dist/controllers/agent-session.controller.js +1 -1
  18. package/dist/controllers/dashboard-user-layout.controller.d.ts +1 -1
  19. package/dist/controllers/dashboard-user-layout.controller.js +1 -1
  20. package/dist/controllers/email-template.controller.d.ts +1 -1
  21. package/dist/controllers/email-template.controller.js +1 -1
  22. package/dist/controllers/export-template.controller.d.ts +1 -1
  23. package/dist/controllers/export-template.controller.d.ts.map +1 -1
  24. package/dist/controllers/export-template.controller.js +6 -4
  25. package/dist/controllers/export-template.controller.js.map +1 -1
  26. package/dist/controllers/export-transaction.controller.d.ts +1 -1
  27. package/dist/controllers/export-transaction.controller.js +1 -1
  28. package/dist/controllers/import-transaction-error-log.controller.d.ts +1 -1
  29. package/dist/controllers/import-transaction-error-log.controller.js +1 -1
  30. package/dist/controllers/import-transaction.controller.d.ts +1 -1
  31. package/dist/controllers/import-transaction.controller.d.ts.map +1 -1
  32. package/dist/controllers/import-transaction.controller.js +7 -7
  33. package/dist/controllers/import-transaction.controller.js.map +1 -1
  34. package/dist/controllers/list-of-values.controller.d.ts +1 -1
  35. package/dist/controllers/list-of-values.controller.js +1 -1
  36. package/dist/controllers/locale.controller.d.ts +1 -1
  37. package/dist/controllers/locale.controller.js +1 -1
  38. package/dist/controllers/mcp-audit-log.controller.d.ts +1 -1
  39. package/dist/controllers/mcp-audit-log.controller.js +1 -1
  40. package/dist/controllers/media.controller.d.ts +1 -1
  41. package/dist/controllers/media.controller.d.ts.map +1 -1
  42. package/dist/controllers/media.controller.js +12 -7
  43. package/dist/controllers/media.controller.js.map +1 -1
  44. package/dist/controllers/menu-item-metadata.controller.d.ts +1 -1
  45. package/dist/controllers/menu-item-metadata.controller.js +1 -1
  46. package/dist/controllers/model-sequence.controller.d.ts +1 -1
  47. package/dist/controllers/model-sequence.controller.js +1 -1
  48. package/dist/controllers/module-package.controller.d.ts.map +1 -1
  49. package/dist/controllers/module-package.controller.js +2 -3
  50. package/dist/controllers/module-package.controller.js.map +1 -1
  51. package/dist/controllers/mq-message-queue.controller.d.ts +1 -1
  52. package/dist/controllers/mq-message-queue.controller.js +1 -1
  53. package/dist/controllers/mq-message.controller.d.ts +1 -1
  54. package/dist/controllers/mq-message.controller.js +1 -1
  55. package/dist/controllers/permission-metadata.controller.d.ts +1 -1
  56. package/dist/controllers/permission-metadata.controller.js +1 -1
  57. package/dist/controllers/role-metadata.controller.d.ts +1 -1
  58. package/dist/controllers/role-metadata.controller.js +1 -1
  59. package/dist/controllers/saved-filters.controller.d.ts +1 -1
  60. package/dist/controllers/saved-filters.controller.js +1 -1
  61. package/dist/controllers/scheduled-job.controller.d.ts +1 -1
  62. package/dist/controllers/scheduled-job.controller.js +1 -1
  63. package/dist/controllers/security-rule.controller.d.ts +1 -1
  64. package/dist/controllers/security-rule.controller.js +1 -1
  65. package/dist/controllers/sms-template.controller.d.ts +1 -1
  66. package/dist/controllers/sms-template.controller.js +1 -1
  67. package/dist/controllers/user-activity-history.controller.d.ts +1 -1
  68. package/dist/controllers/user-activity-history.controller.js +1 -1
  69. package/dist/controllers/user-view-metadata.controller.d.ts +1 -1
  70. package/dist/controllers/user-view-metadata.controller.js +1 -1
  71. package/dist/controllers/user.controller.d.ts +2 -2
  72. package/dist/controllers/user.controller.js +2 -2
  73. package/dist/controllers/view-metadata.controller.d.ts +2 -2
  74. package/dist/controllers/view-metadata.controller.js +1 -1
  75. package/dist/controllers/workflow-definition.controller.d.ts +1 -1
  76. package/dist/controllers/workflow-definition.controller.js +1 -1
  77. package/dist/controllers/workflow-execution-artifact.controller.d.ts +1 -1
  78. package/dist/controllers/workflow-execution-artifact.controller.js +1 -1
  79. package/dist/controllers/workflow-execution-log.controller.d.ts +1 -1
  80. package/dist/controllers/workflow-execution-log.controller.js +1 -1
  81. package/dist/controllers/workflow-execution.controller.d.ts +1 -1
  82. package/dist/controllers/workflow-execution.controller.js +1 -1
  83. package/dist/controllers/workflow-secret.controller.d.ts +1 -1
  84. package/dist/controllers/workflow-secret.controller.js +1 -1
  85. package/dist/controllers/workflow-step-execution.controller.d.ts +1 -1
  86. package/dist/controllers/workflow-step-execution.controller.js +1 -1
  87. package/dist/controllers/workflow-trigger-execution.controller.d.ts +1 -1
  88. package/dist/controllers/workflow-trigger-execution.controller.js +1 -1
  89. package/dist/dtos/basic-filters.dto.d.ts +0 -1
  90. package/dist/dtos/basic-filters.dto.d.ts.map +1 -1
  91. package/dist/dtos/basic-filters.dto.js +1 -6
  92. package/dist/dtos/basic-filters.dto.js.map +1 -1
  93. package/dist/entities/common.entity.d.ts +4 -0
  94. package/dist/entities/common.entity.d.ts.map +1 -1
  95. package/dist/entities/common.entity.js +25 -1
  96. package/dist/entities/common.entity.js.map +1 -1
  97. package/dist/entities/user.entity.js +2 -2
  98. package/dist/entities/user.entity.js.map +1 -1
  99. package/dist/factories/mail.factory.d.ts.map +1 -1
  100. package/dist/factories/mail.factory.js +4 -0
  101. package/dist/factories/mail.factory.js.map +1 -1
  102. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts +1 -0
  103. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
  104. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +18 -11
  105. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
  106. package/dist/helpers/file-download.helper.d.ts +9 -0
  107. package/dist/helpers/file-download.helper.d.ts.map +1 -0
  108. package/dist/helpers/file-download.helper.js +14 -0
  109. package/dist/helpers/file-download.helper.js.map +1 -0
  110. package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
  111. package/dist/helpers/model-metadata-helper.service.js +30 -1
  112. package/dist/helpers/model-metadata-helper.service.js.map +1 -1
  113. package/dist/helpers/solid-registry.js +1 -1
  114. package/dist/helpers/solid-registry.js.map +1 -1
  115. package/dist/index.d.ts +2 -0
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +2 -0
  118. package/dist/index.js.map +1 -1
  119. package/dist/seeders/module-test-data.service.d.ts +3 -1
  120. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  121. package/dist/seeders/module-test-data.service.js +8 -2
  122. package/dist/seeders/module-test-data.service.js.map +1 -1
  123. package/dist/seeders/seed-data/solid-core-metadata.json +2 -0
  124. package/dist/services/authentication.service.d.ts +4 -1
  125. package/dist/services/authentication.service.d.ts.map +1 -1
  126. package/dist/services/authentication.service.js +52 -4
  127. package/dist/services/authentication.service.js.map +1 -1
  128. package/dist/services/chatter-message.service.d.ts +1 -0
  129. package/dist/services/chatter-message.service.d.ts.map +1 -1
  130. package/dist/services/chatter-message.service.js +8 -0
  131. package/dist/services/chatter-message.service.js.map +1 -1
  132. package/dist/services/crud-helper.service.d.ts +5 -2
  133. package/dist/services/crud-helper.service.d.ts.map +1 -1
  134. package/dist/services/crud-helper.service.js +30 -32
  135. package/dist/services/crud-helper.service.js.map +1 -1
  136. package/dist/services/crud.service.d.ts +11 -1
  137. package/dist/services/crud.service.d.ts.map +1 -1
  138. package/dist/services/crud.service.js +79 -66
  139. package/dist/services/crud.service.js.map +1 -1
  140. package/dist/services/datasource-introspection.service.d.ts.map +1 -1
  141. package/dist/services/datasource-introspection.service.js.map +1 -1
  142. package/dist/services/draft-publish-helper.service.d.ts +52 -0
  143. package/dist/services/draft-publish-helper.service.d.ts.map +1 -0
  144. package/dist/services/draft-publish-helper.service.js +368 -0
  145. package/dist/services/draft-publish-helper.service.js.map +1 -0
  146. package/dist/services/internationalisation-helper.service.d.ts +19 -0
  147. package/dist/services/internationalisation-helper.service.d.ts.map +1 -0
  148. package/dist/services/internationalisation-helper.service.js +107 -0
  149. package/dist/services/internationalisation-helper.service.js.map +1 -0
  150. package/dist/services/locale.service.d.ts +1 -1
  151. package/dist/services/media.service.d.ts +2 -1
  152. package/dist/services/media.service.d.ts.map +1 -1
  153. package/dist/services/media.service.js +28 -2
  154. package/dist/services/media.service.js.map +1 -1
  155. package/dist/services/metadata-validation.service.d.ts +7 -0
  156. package/dist/services/metadata-validation.service.d.ts.map +1 -0
  157. package/dist/services/metadata-validation.service.js +42 -0
  158. package/dist/services/metadata-validation.service.js.map +1 -0
  159. package/dist/services/model-metadata.service.d.ts.map +1 -1
  160. package/dist/services/model-metadata.service.js +2 -1
  161. package/dist/services/model-metadata.service.js.map +1 -1
  162. package/dist/services/object.utils.d.ts +2 -0
  163. package/dist/services/object.utils.d.ts.map +1 -0
  164. package/dist/services/object.utils.js +11 -0
  165. package/dist/services/object.utils.js.map +1 -0
  166. package/dist/services/settings/default-settings-provider.service.d.ts +6 -6
  167. package/dist/services/settings/default-settings-provider.service.js +3 -3
  168. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  169. package/dist/services/user.service.d.ts +2 -2
  170. package/dist/services/user.service.d.ts.map +1 -1
  171. package/dist/services/user.service.js +9 -3
  172. package/dist/services/user.service.js.map +1 -1
  173. package/dist/services/view-metadata.service.d.ts +1 -2
  174. package/dist/services/view-metadata.service.d.ts.map +1 -1
  175. package/dist/services/view-metadata.service.js +3 -55
  176. package/dist/services/view-metadata.service.js.map +1 -1
  177. package/dist/services/workflow-definition.service.d.ts +1 -1
  178. package/dist/solid-core.module.d.ts.map +1 -1
  179. package/dist/solid-core.module.js +10 -1
  180. package/dist/solid-core.module.js.map +1 -1
  181. package/package.json +1 -1
  182. package/src/constants/draft-publish-fields.ts +5 -0
  183. package/src/constants/media-file-types.ts +50 -0
  184. package/src/controllers/export-template.controller.ts +5 -3
  185. package/src/controllers/import-transaction.controller.ts +6 -6
  186. package/src/controllers/media.controller.ts +17 -6
  187. package/src/controllers/module-package.controller.ts +2 -3
  188. package/src/dtos/basic-filters.dto.ts +0 -4
  189. package/src/entities/common.entity.ts +20 -0
  190. package/src/entities/user.entity.ts +2 -2
  191. package/src/factories/mail.factory.ts +7 -1
  192. package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +26 -12
  193. package/src/helpers/file-download.helper.ts +31 -0
  194. package/src/helpers/model-metadata-helper.service.ts +32 -1
  195. package/src/helpers/solid-registry.ts +1 -1
  196. package/src/index.ts +2 -0
  197. package/src/seeders/module-test-data.service.ts +6 -0
  198. package/src/seeders/seed-data/solid-core-metadata.json +2 -0
  199. package/src/services/1.js +6 -0
  200. package/src/services/authentication.service.ts +76 -5
  201. package/src/services/chatter-message.service.ts +16 -0
  202. package/src/services/crud-helper.service.ts +16 -34
  203. package/src/services/crud.service.ts +98 -103
  204. package/src/services/datasource-introspection.service.ts +5 -0
  205. package/src/services/draft-publish-helper.service.ts +529 -0
  206. package/src/services/internationalisation-helper.service.ts +148 -0
  207. package/src/services/media.service.ts +48 -3
  208. package/src/services/metadata-validation.service.ts +39 -0
  209. package/src/services/model-metadata.service.ts +4 -1
  210. package/src/services/object.utils.ts +10 -0
  211. package/src/services/settings/default-settings-provider.service.ts +3 -3
  212. package/src/services/user.service.ts +18 -3
  213. package/src/services/view-metadata.service.ts +5 -145
  214. package/src/solid-core.module.ts +16 -3
  215. package/dist-tests/api/authenticate.spec.js +0 -119
  216. package/dist-tests/api/authenticate.spec.js.map +0 -1
  217. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
  218. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
  219. package/dist-tests/api/ping.spec.js +0 -21
  220. package/dist-tests/api/ping.spec.js.map +0 -1
  221. package/dist-tests/helpers/auth.js +0 -41
  222. package/dist-tests/helpers/auth.js.map +0 -1
  223. package/dist-tests/helpers/env.js +0 -11
  224. package/dist-tests/helpers/env.js.map +0 -1
  225. package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
  226. package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
  227. package/docs/agent-hub-grooming.md +0 -301
  228. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
  229. package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
  230. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
  231. package/docs/datasource-introspection-ddl-analysis.md +0 -326
  232. package/docs/datasource-introspection-implementation-plan.md +0 -306
  233. package/docs/grouping-enhancements.md +0 -89
  234. package/docs/java-spring/README.md +0 -3
  235. package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
  236. package/docs/module-package-import-handoff.md +0 -691
  237. package/docs/seed-changes.md +0 -65
  238. package/docs/test-data-workflow.md +0 -200
  239. package/docs/type-declaration-import-issue.md +0 -24
  240. package/docs/workflow/business-automation-example-notes.md +0 -309
  241. package/docs/workflow/control-flow-node-addition-sop.md +0 -324
  242. package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
  243. package/docs/workflow/foreach-example-notes.md +0 -187
  244. package/docs/workflow/hello-world-example-notes.md +0 -271
  245. package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
  246. package/docs/workflow/loop-until-example-notes.md +0 -198
  247. package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
  248. package/docs/workflow/samples.md +0 -25
  249. package/docs/workflow/what-is-kestra.md +0 -79
  250. package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
  251. package/docs/workflow/workflow-module-brd.md +0 -185
  252. package/docs/workflow/workflow-module-domain-model.md +0 -252
  253. package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
  254. package/docs/workflow/workflow-module-technical-spec.md +0 -293
  255. package/docs/workflow/workflow-node-type-contracts.md +0 -635
  256. package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
  257. package/docs/workflow/workflow-solidx-model-field-spec.md +0 -284
@@ -36,12 +36,12 @@ export class User extends CommonEntity {
36
36
  @Expose()
37
37
  username: string;
38
38
 
39
- @Index()
39
+ @Index({ unique: true })
40
40
  @Column({ type: "varchar", nullable: true })
41
41
  @Expose()
42
42
  email: string;
43
43
 
44
- @Index()
44
+ @Index({ unique: true })
45
45
  @Column({ type: "varchar", nullable: true })
46
46
  @Expose()
47
47
  mobile: string;
@@ -30,8 +30,14 @@ export class MailFactory {
30
30
  }
31
31
 
32
32
  const mailServiceProvider = mailProviders.find(provider => provider.name === mailServiceName);
33
+ if (!mailServiceProvider?.instance) {
34
+ const registeredProviders = mailProviders.map((provider) => provider?.name).filter(Boolean).join(", ");
35
+ throw new Error(
36
+ `Mail provider "${mailServiceName}" is not registered. Registered providers: ${registeredProviders || "none"}`,
37
+ );
38
+ }
33
39
 
34
40
  return mailServiceProvider.instance as IMail;
35
41
  }
36
42
 
37
- }
43
+ }
@@ -1,5 +1,5 @@
1
1
  import { FieldCrudManager, ValidationError } from "src/interfaces";
2
- import { DANGEROUS_EXTENSIONS, EXT_TO_MEDIA_TYPE, MediaCategory } from "src/constants/media-file-types";
2
+ import { EXT_TO_MEDIA_TYPE, getLowercaseFileExtension, getUploadedFileName, isDangerousMediaFile, MediaCategory } from "src/constants/media-file-types";
3
3
 
4
4
  export enum SolidMediaType {
5
5
  mediaSingle = 'mediaSingle',
@@ -79,16 +79,15 @@ export class MediaFieldCrudManager implements FieldCrudManager {
79
79
  constructor(private readonly options: MediaFieldOptions) {
80
80
  }
81
81
 
82
- private resolveMediaType(mimetype?: string, filename?: string): MediaType | null {
83
- const ext = (filename || '').split('.').pop()?.toLowerCase();
84
- if (ext && DANGEROUS_EXTENSIONS.has(ext)) {
82
+ private resolveMediaType(file: Express.Multer.File): MediaType | null {
83
+ // Belt-and-braces: dangerous files are already rejected outright by
84
+ // validateDangerousFiles, but never let one resolve to a usable category.
85
+ if (isDangerousMediaFile(file)) {
85
86
  return null;
86
87
  }
87
88
 
88
- const mt = (mimetype || '').toLowerCase().trim();
89
- if (mt === 'image/svg+xml' || mt === 'text/html' || mt === 'application/xhtml+xml') {
90
- return null;
91
- }
89
+ const ext = getLowercaseFileExtension(getUploadedFileName(file));
90
+ const mt = (file.mimetype || '').toLowerCase().trim();
92
91
  if (mt && MIME_TO_MEDIA_TYPE[mt]) {
93
92
  return MIME_TO_MEDIA_TYPE[mt];
94
93
  }
@@ -113,16 +112,31 @@ export class MediaFieldCrudManager implements FieldCrudManager {
113
112
  }
114
113
 
115
114
  private applyValidations(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
115
+ // Runs for every media field type and regardless of the admin-configured mediaTypes
116
+ // allowlist - a dangerous upload is never acceptable, so this must not sit behind
117
+ // the optional type checks below.
118
+ const errors = this.validateDangerousFiles(fieldFiles);
119
+
116
120
  switch (this.options.type) {
117
121
  case SolidMediaType.mediaSingle:
118
- return this.validateMediaSingle(fieldFiles);
122
+ return [...errors, ...this.validateMediaSingle(fieldFiles)];
119
123
  case SolidMediaType.mediaMultiple:
120
- return this.validateMediaMultiple(fieldFiles);
124
+ return [...errors, ...this.validateMediaMultiple(fieldFiles)];
121
125
  default:
122
- return [];
126
+ return errors;
123
127
  }
124
128
  }
125
129
 
130
+ private validateDangerousFiles(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
131
+ return fieldFiles
132
+ .filter(fieldFile => isDangerousMediaFile(fieldFile))
133
+ .map(fieldFile => ({
134
+ field: this.options.fieldName,
135
+ error: `${this.options.fieldName} file type not allowed. ` +
136
+ `${getUploadedFileName(fieldFile) || 'File'} is a potentially dangerous file type.`
137
+ }));
138
+ }
139
+
126
140
  private validateMediaSingle(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
127
141
  const errors: ValidationError[] = [];
128
142
  if (!this.options.isUpdate && this.options.required && fieldFiles.length === 0) {
@@ -157,7 +171,7 @@ export class MediaFieldCrudManager implements FieldCrudManager {
157
171
  for (let i = 0; i < fieldFiles.length; i++) {
158
172
  const fieldFile = fieldFiles[i];
159
173
 
160
- const resolvedType = this.resolveMediaType(fieldFile.mimetype, fieldFile.originalname ?? fieldFile.filename ?? '');
174
+ const resolvedType = this.resolveMediaType(fieldFile);
161
175
  if (!resolvedType || !allowedFileTypes.includes(resolvedType)) {
162
176
  errors.push({
163
177
  field: this.options.fieldName,
@@ -0,0 +1,31 @@
1
+ import type { Response } from "express";
2
+
3
+ export interface FileDownloadHeaderOptions {
4
+ fileName: string;
5
+ mimeType: string;
6
+ /** Defaults to 'attachment'; only pass 'inline' for types known safe to render. */
7
+ disposition?: "inline" | "attachment";
8
+ /** Opt in for assets loaded cross-origin by the UI (e.g. media served to :3000). */
9
+ crossOriginResourcePolicy?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Single place that stamps the response headers for every streamed file download, so the
14
+ * security headers can't be applied to one endpoint and forgotten on the next.
15
+ */
16
+ export function setFileDownloadHeaders(res: Response, options: FileDownloadHeaderOptions): void {
17
+ const disposition = options.disposition ?? "attachment";
18
+
19
+ res.setHeader("Content-Disposition", `${disposition}; filename="${options.fileName}"`);
20
+ res.setHeader("Content-Type", options.mimeType);
21
+ res.setHeader("Access-Control-Expose-Headers", "Content-Disposition, Content-Type");
22
+
23
+ // Prevent the browser from ever content-sniffing a served file into something more
24
+ // dangerous than its declared Content-Type (e.g. rendering a mislabeled upload as HTML
25
+ // and executing embedded script).
26
+ res.setHeader("X-Content-Type-Options", "nosniff");
27
+
28
+ if (options.crossOriginResourcePolicy) {
29
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
30
+ }
31
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common";
4
4
  import { _ } from "lodash";
5
+ import { DRAFT_PUBLISH_VERSIONING_FIELD_NAMES } from "src/constants/draft-publish-fields";
5
6
  import { LEGACY_TABLE_FIELDS_PREFIX } from "src/entities/legacy-common.entity-with-existing-id.entity";
6
7
  import { LegacyTableType } from "src/enums/legacy-table-type.enum";
7
8
  import { ModelMetadataRepository } from "src/repository/model-metadata.repository";
@@ -87,6 +88,32 @@ export class ModelMetadataHelperService {
87
88
  isSystem: true,
88
89
  enableAuditTracking:true
89
90
  },
91
+ {
92
+ name: "isPublished",
93
+ displayName: "Is Published",
94
+ type: "boolean",
95
+ isSystem: true,
96
+ enableAuditTracking: true
97
+ },
98
+ {
99
+ name: "isLatest",
100
+ displayName: "Is Latest",
101
+ type: "boolean",
102
+ isSystem: true,
103
+ enableAuditTracking: true
104
+ },
105
+ {
106
+ name: "initialEntityVersionId",
107
+ displayName: "Initial Entity Version Id",
108
+ type: "int",
109
+ isSystem: true,
110
+ },
111
+ {
112
+ name: "publishedTracker",
113
+ displayName: "Published Tracker",
114
+ type: "shortText",
115
+ isSystem: true,
116
+ },
90
117
  {
91
118
  name: "localeName",
92
119
  displayName: "Locale",
@@ -117,7 +144,11 @@ export class ModelMetadataHelperService {
117
144
  }
118
145
 
119
146
  private getSystemFieldsMetadataMappingForLegacyTable(withId: boolean=true) {
120
- const systemFieldsMetadata = this.getSystemFieldsMetadataMapping();
147
+ // Legacy tables don't have the draft/publish versioning columns, so exclude
148
+ // them from the legacy system fields metadata mapping.
149
+ const legacyExcludedFieldNames: readonly string[] = DRAFT_PUBLISH_VERSIONING_FIELD_NAMES;
150
+ const systemFieldsMetadata = this.getSystemFieldsMetadataMapping()
151
+ .filter(field => !legacyExcludedFieldNames.includes(field.name));
121
152
  if (!withId) {
122
153
  // Remove the id field metadata
123
154
  const index = systemFieldsMetadata.findIndex(field => field.name === 'id');
@@ -366,7 +366,7 @@ export class SolidRegistry {
366
366
  }
367
367
 
368
368
  getCommonEntityKeys(): (keyof CommonEntity | 'createdBy' | 'updatedBy')[] {
369
- return ['id', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'deletedTracker', 'localeName', 'defaultEntityLocaleId', 'publishedAt'];
369
+ return ['id', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'deletedTracker', 'localeName', 'defaultEntityLocaleId', 'publishedAt', 'isPublished', 'isLatest', 'initialEntityVersionId', 'publishedTracker'];
370
370
  // return Reflect.getMetadataKeys(CommonEntity.prototype) as (keyof CommonEntity)[];
371
371
  }
372
372
 
package/src/index.ts CHANGED
@@ -225,6 +225,7 @@ export * from './helpers/environment.helper'
225
225
  export * from './helpers/cache.helper'
226
226
  export * from './helpers/cors.helper'
227
227
  export * from './helpers/security.helper'
228
+ export * from './helpers/file-download.helper'
228
229
  export * from './helpers/model-metadata-helper.service'
229
230
  export * from './helpers/image-encoding.helper'
230
231
  export * from './helpers/solid-microservice-adapter.service'
@@ -337,6 +338,7 @@ export * from './services/action-metadata.service'
337
338
  export * from './services/authentication.service'
338
339
  export * from './services/bcrypt.service'
339
340
  export * from './services/computed-fields/uuid-external-id-computed-field-provider.service'
341
+ export * from './services/draft-publish-helper.service'
340
342
  export * from './services/crud-helper.service'
341
343
  export * from './services/crud.service'
342
344
  export * from './services/email-template.service'
@@ -12,6 +12,7 @@ import { CreateModelMetadataDto } from 'src/dtos/create-model-metadata.dto';
12
12
  import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
13
13
  import { getDynamicModuleNamesBasedOnMetadata } from 'src/helpers/module.helper';
14
14
  import { SolidRegistry } from 'src/helpers/solid-registry';
15
+ import { InternationalisationHelperService } from 'src/services/internationalisation-helper.service';
15
16
  import { MediaRepository } from 'src/repository/media.repository';
16
17
  import { PermissionMetadataRepository } from 'src/repository/permission-metadata.repository';
17
18
  import { AuthenticationService } from 'src/services/authentication.service';
@@ -30,6 +31,7 @@ export class ModuleTestDataService {
30
31
  private readonly moduleRef: ModuleRef,
31
32
  private readonly discoveryService: DiscoveryService,
32
33
  private readonly solidRegistry: SolidRegistry,
34
+ private readonly internationalisationHelperService: InternationalisationHelperService,
33
35
  ) {}
34
36
 
35
37
  async setupTestData(modulesToTest?: string[]): Promise<void> {
@@ -246,6 +248,10 @@ export class ModuleTestDataService {
246
248
  const entityRepo = this.resolveRepository(modelUserKey);
247
249
  const payload: Record<string, any> = { ...(entry.data ?? {}) };
248
250
 
251
+ if (modelDef.internationalisation && !payload.localeName) {
252
+ payload.localeName = this.internationalisationHelperService.resolveDefaultLocaleName(this.solidRegistry);
253
+ }
254
+
249
255
  for (const field of modelDef.fields ?? []) {
250
256
  if (field.type === 'relation' && field.relationType === 'many-to-one') {
251
257
  const userKeyProp = `${field.name}UserKey`;
@@ -1808,6 +1808,8 @@
1808
1808
  "index": true,
1809
1809
  "private": false,
1810
1810
  "encrypt": false,
1811
+ "regexPattern": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
1812
+ "regexPatternNotMatchingErrorMsg": "Please enter a valid email address.",
1811
1813
  "isSystem": true,
1812
1814
  "enableAuditTracking": true
1813
1815
  },
@@ -0,0 +1,6 @@
1
+ 1. Do i need to create a storeStreams method for aws service too?
2
+ - Handle later
3
+ 2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
4
+ - startExport should either return the data or return the transaction id
5
+ 3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
6
+ - show the userKey
@@ -20,7 +20,7 @@ import { SUCCESS_MESSAGES } from "src/constants/success-messages";
20
20
  import { CreateUserDto } from "src/dtos/create-user.dto";
21
21
  import { MailFactory } from "src/factories/mail.factory";
22
22
  import { UserRepository } from "src/repository/user.repository";
23
- import { DataSource, DeepPartial, Repository } from "typeorm";
23
+ import { DataSource, DeepPartial, FindOptionsWhere, Repository } from "typeorm";
24
24
  import { v4 as uuidv4 } from "uuid";
25
25
  import {
26
26
  ForgotPasswordSendVerificationTokenOn,
@@ -49,6 +49,7 @@ import { SsoCodeStorageService } from "./sso-code-storage.service";
49
49
  import { RoleMetadataService } from "./role-metadata.service";
50
50
  import { SettingService } from "./setting.service";
51
51
  import { UserActivityHistoryService } from "./user-activity-history.service";
52
+ import { MetadataValidationService } from "./metadata-validation.service";
52
53
  import { UserService } from "./user.service";
53
54
  import { SmsFactory } from "src/factories/sms.factory";
54
55
  import { WhatsAppFactory } from "src/factories/whatsapp.factory";
@@ -88,6 +89,7 @@ export class AuthenticationService {
88
89
  private readonly roleMetadataService: RoleMetadataService,
89
90
  private readonly userActivityHistoryService: UserActivityHistoryService,
90
91
  private readonly ssoCodeStorage: SsoCodeStorageService,
92
+ private readonly metadataValidationService: MetadataValidationService,
91
93
 
92
94
  @InjectDataSource()
93
95
  private readonly dataSource: DataSource,
@@ -225,6 +227,9 @@ export class AuthenticationService {
225
227
  preferEntityApiKeyFlag: boolean = false,
226
228
  ): Promise<T> {
227
229
  try {
230
+ await this.assertUniqueSignupIdentifiers(signUpDto, repo);
231
+ await this.metadataValidationService.validateCreateDto("user", signUpDto);
232
+
228
233
  const onForcePasswordChange =
229
234
  this.settingService.getConfigValue<SolidCoreSetting>(
230
235
  "forceChangePasswordOnFirstLogin",
@@ -276,6 +281,59 @@ export class AuthenticationService {
276
281
  }
277
282
  }
278
283
 
284
+ private async assertUniqueSignupIdentifiers<T extends User>(
285
+ signUpDto: SignUpDto,
286
+ repo: Repository<T>,
287
+ ): Promise<void> {
288
+ const username = signUpDto.username?.trim();
289
+ const email = signUpDto.email?.trim();
290
+ const mobile = signUpDto.mobile?.trim();
291
+
292
+ const where: FindOptionsWhere<T>[] = [];
293
+
294
+ if (username) {
295
+ where.push({ username } as FindOptionsWhere<T>);
296
+ }
297
+
298
+ if (email) {
299
+ where.push({ email } as FindOptionsWhere<T>);
300
+ }
301
+
302
+ if (mobile) {
303
+ where.push({ mobile } as FindOptionsWhere<T>);
304
+ }
305
+
306
+ if (where.length === 0) {
307
+ return;
308
+ }
309
+
310
+ const existingUser = await repo.findOne({ where });
311
+
312
+ if (!existingUser) {
313
+ return;
314
+ }
315
+
316
+ if (email && existingUser.email === email) {
317
+ throw new ConflictException(
318
+ `A user with email address "${email}" already exists.`,
319
+ );
320
+ }
321
+
322
+ if (mobile && existingUser.mobile === mobile) {
323
+ throw new ConflictException(
324
+ `A user with mobile number "${mobile}" already exists.`,
325
+ );
326
+ }
327
+
328
+ if (username && existingUser.username === username) {
329
+ throw new ConflictException(
330
+ `A user with username "${username}" already exists.`,
331
+ );
332
+ }
333
+
334
+ throw new ConflictException(ERROR_MESSAGES.USER_ALREADY_EXISTS);
335
+ }
336
+
279
337
  private async handlePasswordlessSignupOtp<T extends User>(
280
338
  user: T,
281
339
  signUpDto: SignUpDto,
@@ -400,11 +458,23 @@ export class AuthenticationService {
400
458
  // if forcePasswordChange is true, then we trigger an email to the user to change the password, this needs to be done using a queue.
401
459
  // Create a new method like notifyUserOnForcePasswordChange, create a new email template we can call it on-force-password-change this template to include the random password
402
460
  if (user.forcePasswordChange && autoGeneratedPwd) {
403
- await this.notifyUserOnForcePasswordChange(user, autoGeneratedPwd);
461
+ try {
462
+ await this.notifyUserOnForcePasswordChange(user, autoGeneratedPwd);
463
+ } catch (error: any) {
464
+ this.logger.error(
465
+ `Failed to send force-password-change email for user ${user.id}: ${error?.message ?? error}`,
466
+ );
467
+ }
404
468
  }
405
469
 
406
470
  // Send welcome notifications (email/SMS) if enabled.
407
- await this.notifyUserOnSignup(user);
471
+ try {
472
+ await this.notifyUserOnSignup(user);
473
+ } catch (error: any) {
474
+ this.logger.error(
475
+ `Failed to send signup notifications for user ${user.id}: ${error?.message ?? error}`,
476
+ );
477
+ }
408
478
  }
409
479
 
410
480
  generatePassword(length: number = 8): string {
@@ -1578,7 +1648,7 @@ export class AuthenticationService {
1578
1648
  // Assuming all users do not have mobile as mandatory.
1579
1649
  if (
1580
1650
  forgotPasswordSendVerificationTokenOn ==
1581
- ForgotPasswordSendVerificationTokenOn.MOBILE &&
1651
+ ForgotPasswordSendVerificationTokenOn.MOBILE &&
1582
1652
  user.mobile
1583
1653
  ) {
1584
1654
  const smsService = this.smsFactory.getSmsService();
@@ -1702,7 +1772,7 @@ export class AuthenticationService {
1702
1772
  // Assuming all users do not have mobile as mandatory.
1703
1773
  if (
1704
1774
  forgotPasswordSendVerificationTokenOn ==
1705
- ForgotPasswordSendVerificationTokenOn.MOBILE &&
1775
+ ForgotPasswordSendVerificationTokenOn.MOBILE &&
1706
1776
  user.mobile
1707
1777
  ) {
1708
1778
  const smsService = this.smsFactory.getSmsService();
@@ -2342,6 +2412,7 @@ function parseUniqueConstraintError(detail: string): string {
2342
2412
  const fieldMap: Record<string, string> = {
2343
2413
  username: "username",
2344
2414
  email: "email address",
2415
+ mobile: "mobile number",
2345
2416
  full_name_user_key: "full name",
2346
2417
  };
2347
2418
  const friendlyField = fieldMap[field] || field;
@@ -6,6 +6,7 @@ import { Brackets, EntityManager, EntityMetadata, In } from 'typeorm';
6
6
 
7
7
  import { classify } from '@angular-devkit/core/src/utils/strings';
8
8
  import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE, CHATTER_MESSAGE_USER_FIELDS } from 'src/constants/chatter-message.constants';
9
+ import { isDangerousMediaFile } from 'src/constants/media-file-types';
9
10
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
10
11
  import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto';
11
12
  import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto';
@@ -174,6 +175,17 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
174
175
  }, messageBody || '');
175
176
  }
176
177
 
178
+ /**
179
+ * Chatter attachments bypass MediaFieldCrudManager - they go straight to
180
+ * storageProvider.store() - so the shared predicate is applied here directly. The rules
181
+ * themselves live in media-file-types so upload paths can't drift apart.
182
+ */
183
+ private validateChatterMediaFiles(files: Express.Multer.File[] = []) {
184
+ if (Array.isArray(files) && files.some(file => isDangerousMediaFile(file))) {
185
+ throw new BadRequestException('Dangerous file types are not allowed in chatter attachments.');
186
+ }
187
+ }
188
+
177
189
  private async publishChatterMentionNotifications(message: ChatterMessage, model: any) {
178
190
  if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM || message.messageSubType !== CHATTER_MESSAGE_SUBTYPE.NOTE) {
179
191
  return;
@@ -305,6 +317,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
305
317
  throw new BadRequestException('Message body cannot be empty.');
306
318
  }
307
319
 
320
+ this.validateChatterMediaFiles(files);
321
+
308
322
  if (hasMessageBody) {
309
323
  message.messageBody = trimmedMessageBody;
310
324
  }
@@ -379,6 +393,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
379
393
  const coModelName = lowerFirst(postDto.coModelName);
380
394
  await this.assertRecordAccess(coModelName, postDto.coModelEntityId);
381
395
 
396
+ this.validateChatterMediaFiles(files);
397
+
382
398
  const chatterMessage = new ChatterMessage();
383
399
  chatterMessage.messageType = CHATTER_MESSAGE_TYPE.CUSTOM;
384
400
  chatterMessage.messageSubType = postDto.messageSubType || CHATTER_MESSAGE_SUBTYPE.CUSTOM;
@@ -3,9 +3,12 @@ import { BasicFilterDto } from "../dtos/basic-filters.dto";
3
3
  import { classify } from '../helpers/string.helper';
4
4
  import { ActiveUserData } from "src/interfaces/active-user-data.interface";
5
5
  import { SolidRegistry } from "src/helpers/solid-registry";
6
- import { BadRequestException, Logger } from "@nestjs/common";
6
+ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
7
7
  import { ERROR_MESSAGES } from "src/constants/error-messages";
8
8
  import { buildCastToText } from "src/helpers/typeorm-db-helper";
9
+ import { DraftPublishHelperService } from "./draft-publish-helper.service";
10
+ import { InternationalisationHelperService } from "./internationalisation-helper.service";
11
+ import { normalizeObjectKeys } from "./object.utils";
9
12
 
10
13
  export enum FilterCombinator {
11
14
  AND = '$and',
@@ -36,10 +39,14 @@ export interface ResolvedFieldPath {
36
39
  leafIsRelation: boolean;
37
40
  }
38
41
 
42
+ @Injectable()
39
43
  export class CrudHelperService {
44
+ private readonly logger = new Logger(CrudHelperService.name);
45
+
40
46
  constructor(
47
+ private readonly draftPublishHelperService: DraftPublishHelperService,
48
+ private readonly internationalisationHelperService: InternationalisationHelperService,
41
49
  ) { }
42
- private readonly logger = new Logger(CrudHelperService.name);
43
50
 
44
51
  /**
45
52
  * Resolve a user-supplied dotted path (e.g. "customer.name") against real TypeORM metadata.
@@ -163,7 +170,7 @@ export class CrudHelperService {
163
170
  }
164
171
 
165
172
  applyFilters(qb: WhereExpressionBuilder, filters: any, alias: string = 'entity', selectQb: SelectQueryBuilder<any>) {
166
- const normalizedFilters = this.normalizeObjectKeys(filters);
173
+ const normalizedFilters = normalizeObjectKeys(filters);
167
174
  if (normalizedFilters.$and) {
168
175
  normalizedFilters.$and.forEach((andFilter: any) => {
169
176
  qb.andWhere(
@@ -182,7 +189,7 @@ export class CrudHelperService {
182
189
  // For individual conditions
183
190
  Object.keys(normalizedFilters).forEach(key => {
184
191
  const primaryFilterObj = normalizedFilters[key];
185
- const normalizedPrimaryFilterObj = this.normalizeObjectKeys(primaryFilterObj);
192
+ const normalizedPrimaryFilterObj = normalizeObjectKeys(primaryFilterObj);
186
193
 
187
194
  const [rawField, funcAlias] = key.split(':');
188
195
 
@@ -312,17 +319,6 @@ export class CrudHelperService {
312
319
  }
313
320
  }
314
321
 
315
- // Normalize the primary operator object by removing the surrounding brackets in the keys e.g [$eq] => $eq
316
- private normalizeObjectKeys(obj: any): any {
317
- return Object.keys(obj).reduce((acc, key) => {
318
- // Transform the key by removing surrounding brackets
319
- const newKey = key.replace(/^\[(.*)\]$/, '$1');
320
- // Assign the value to the transformed key in the accumulator object
321
- acc[newKey] = obj[key];
322
- return acc;
323
- }, {});
324
- }
325
-
326
322
  normalize(value: string | string[]): string[] {
327
323
  if (!value) return []; // if the value is nullish, then return an empty array
328
324
  return Array.isArray(value) ? value : [value]; // if the value is an array, return it as is, otherwise return it as an array
@@ -359,7 +355,7 @@ export class CrudHelperService {
359
355
  applySorting: boolean = true
360
356
  ): SelectQueryBuilder<any> { // TODO : Check how to pass a type to SelectQueryBuilder instead of any
361
357
  let { limit, offset, showSoftDeleted, filters } = basicFilterDto;
362
- const { fields, sort, populate = [], populateMedia = [], locale, status } = basicFilterDto;
358
+ const { fields, sort, populate = [], populateMedia = [], locale } = basicFilterDto;
363
359
 
364
360
  // Normalize the fields, sort, groupBy and populate options i.e (since they can be either a string or an array of strings, when coming from the request)
365
361
  const normalizedFields = this.normalize(fields);
@@ -390,28 +386,14 @@ export class CrudHelperService {
390
386
  }
391
387
  }
392
388
 
393
- let finalLocale = locale
394
389
  if (internationalisation) {
395
- // If locale is not provided in the filter dto, then assume it is the default locale to be used.
396
- if (!finalLocale) {
397
- //Get default locale from registry
398
- const solidRegistry = moduleRef.get(SolidRegistry, { strict: false });
399
- const defaultLocale = solidRegistry.getDefaultLocale();
400
- if(defaultLocale){
401
- finalLocale = defaultLocale.locale;
402
- }else{
403
- finalLocale = 'en';
404
- }
405
- }
390
+ // If locale is not provided in the filter dto, then assume it is the default locale to be used.
391
+ const finalLocale = locale || this.internationalisationHelperService.resolveDefaultLocaleName(moduleRef.get(SolidRegistry, { strict: false }));
406
392
  qb.andWhere(`${entityAlias}.localeName = :locale`, { locale: finalLocale });
407
393
  }
408
394
 
409
- if (draftPublishWorkflow && status) {
410
- if (basicFilterDto.status === 'published') {
411
- qb.andWhere(`${entityAlias}.publishedAt IS NOT NULL`);
412
- } else if (basicFilterDto.status === 'draft') {
413
- qb.andWhere(`${entityAlias}.publishedAt IS NULL`);
414
- }
395
+ if (draftPublishWorkflow) {
396
+ this.draftPublishHelperService.applyDraftPublishFilterDefaults(qb, filters, entityAlias);
415
397
  }
416
398
  // Depending upon the select option, apply the select clause
417
399
  if (normalizedFields && normalizedFields.length) {