@solidxai/core 0.1.13-beta.12 → 0.1.13-beta.14

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 (115) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/CHANGELOG.md +1143 -0
  3. package/CLAUDE.md +26 -0
  4. package/README.md +19 -0
  5. package/dist/constants/media-file-types.d.ts +1 -0
  6. package/dist/constants/media-file-types.d.ts.map +1 -1
  7. package/dist/constants/media-file-types.js +2 -1
  8. package/dist/constants/media-file-types.js.map +1 -1
  9. package/dist/controllers/field-metadata.controller.d.ts +2 -2
  10. package/dist/dtos/create-field-metadata.dto.d.ts +2 -6
  11. package/dist/dtos/create-field-metadata.dto.d.ts.map +1 -1
  12. package/dist/dtos/create-field-metadata.dto.js +4 -10
  13. package/dist/dtos/create-field-metadata.dto.js.map +1 -1
  14. package/dist/dtos/create-model-sequence.dto.d.ts +1 -1
  15. package/dist/dtos/create-model-sequence.dto.d.ts.map +1 -1
  16. package/dist/dtos/create-model-sequence.dto.js +1 -1
  17. package/dist/dtos/create-model-sequence.dto.js.map +1 -1
  18. package/dist/helpers/bootstrap.helper.d.ts +2 -1
  19. package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
  20. package/dist/helpers/bootstrap.helper.js +2 -2
  21. package/dist/helpers/bootstrap.helper.js.map +1 -1
  22. package/dist/helpers/security.helper.d.ts +5 -1
  23. package/dist/helpers/security.helper.d.ts.map +1 -1
  24. package/dist/helpers/security.helper.js +15 -4
  25. package/dist/helpers/security.helper.js.map +1 -1
  26. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  27. package/dist/seeders/module-metadata-seeder.service.js +7 -3
  28. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  29. package/dist/seeders/seed-data/solid-core-metadata.json +1 -1
  30. package/dist/services/chatter-message.service.d.ts +4 -1
  31. package/dist/services/chatter-message.service.d.ts.map +1 -1
  32. package/dist/services/chatter-message.service.js +4 -1
  33. package/dist/services/chatter-message.service.js.map +1 -1
  34. package/dist/services/crud-helper.service.d.ts +1 -2
  35. package/dist/services/crud-helper.service.d.ts.map +1 -1
  36. package/dist/services/crud-helper.service.js +2 -16
  37. package/dist/services/crud-helper.service.js.map +1 -1
  38. package/dist/services/crud.service.d.ts.map +1 -1
  39. package/dist/services/crud.service.js +1 -1
  40. package/dist/services/crud.service.js.map +1 -1
  41. package/dist/services/draft-publish-helper.service.d.ts +1 -0
  42. package/dist/services/draft-publish-helper.service.d.ts.map +1 -1
  43. package/dist/services/draft-publish-helper.service.js +38 -4
  44. package/dist/services/draft-publish-helper.service.js.map +1 -1
  45. package/dist/services/field-metadata.service.d.ts +2 -2
  46. package/dist/services/field-metadata.service.d.ts.map +1 -1
  47. package/dist/services/field-metadata.service.js +2 -1
  48. package/dist/services/field-metadata.service.js.map +1 -1
  49. package/dist/services/settings/default-settings-provider.service.d.ts +62 -6
  50. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  51. package/dist/services/settings/default-settings-provider.service.js +34 -3
  52. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  53. package/dist/theme/theme-registry.d.ts +14 -0
  54. package/dist/theme/theme-registry.d.ts.map +1 -0
  55. package/dist/theme/theme-registry.js +147 -0
  56. package/dist/theme/theme-registry.js.map +1 -0
  57. package/package.json +1 -1
  58. package/src/constants/media-file-types.ts +2 -0
  59. package/src/dtos/create-field-metadata.dto.ts +4 -9
  60. package/src/dtos/create-model-sequence.dto.ts +1 -1
  61. package/src/helpers/bootstrap.helper.ts +17 -3
  62. package/src/helpers/security.helper.ts +35 -8
  63. package/src/seeders/module-metadata-seeder.service.ts +11 -3
  64. package/src/seeders/seed-data/solid-core-metadata.json +1 -1
  65. package/src/services/1.js +6 -0
  66. package/src/services/chatter-message.service.ts +22 -1
  67. package/src/services/crud-helper.service.ts +19 -16
  68. package/src/services/crud.service.ts +2 -1
  69. package/src/services/draft-publish-helper.service.ts +56 -4
  70. package/src/services/field-metadata.service.ts +3 -2
  71. package/src/services/settings/default-settings-provider.service.ts +34 -3
  72. package/src/theme/theme-registry.ts +181 -0
  73. package/dist-tests/api/authenticate.spec.js +0 -119
  74. package/dist-tests/api/authenticate.spec.js.map +0 -1
  75. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
  76. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
  77. package/dist-tests/api/ping.spec.js +0 -21
  78. package/dist-tests/api/ping.spec.js.map +0 -1
  79. package/dist-tests/helpers/auth.js +0 -41
  80. package/dist-tests/helpers/auth.js.map +0 -1
  81. package/dist-tests/helpers/env.js +0 -11
  82. package/dist-tests/helpers/env.js.map +0 -1
  83. package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
  84. package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
  85. package/docs/agent-hub-grooming.md +0 -301
  86. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
  87. package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
  88. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
  89. package/docs/datasource-introspection-ddl-analysis.md +0 -326
  90. package/docs/datasource-introspection-implementation-plan.md +0 -306
  91. package/docs/grouping-enhancements.md +0 -89
  92. package/docs/java-spring/README.md +0 -3
  93. package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
  94. package/docs/module-package-import-handoff.md +0 -691
  95. package/docs/seed-changes.md +0 -65
  96. package/docs/test-data-workflow.md +0 -200
  97. package/docs/type-declaration-import-issue.md +0 -24
  98. package/docs/workflow/business-automation-example-notes.md +0 -309
  99. package/docs/workflow/control-flow-node-addition-sop.md +0 -324
  100. package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
  101. package/docs/workflow/foreach-example-notes.md +0 -187
  102. package/docs/workflow/hello-world-example-notes.md +0 -271
  103. package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
  104. package/docs/workflow/loop-until-example-notes.md +0 -198
  105. package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
  106. package/docs/workflow/samples.md +0 -25
  107. package/docs/workflow/what-is-kestra.md +0 -79
  108. package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
  109. package/docs/workflow/workflow-module-brd.md +0 -185
  110. package/docs/workflow/workflow-module-domain-model.md +0 -252
  111. package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
  112. package/docs/workflow/workflow-module-technical-spec.md +0 -293
  113. package/docs/workflow/workflow-node-type-contracts.md +0 -635
  114. package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
  115. package/docs/workflow/workflow-solidx-model-field-spec.md +0 -284
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getThemeModeFromThemeKey = getThemeModeFromThemeKey;
7
+ exports.getCoreThemes = getCoreThemes;
8
+ exports.getRegisteredThemes = getRegisteredThemes;
9
+ exports.getThemesByMode = getThemesByMode;
10
+ exports.getDefaultThemeKey = getDefaultThemeKey;
11
+ exports.isRegisteredThemeKey = isRegisteredThemeKey;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const DEFAULT_THEME_KEYS = {
15
+ light: "solid-light-purple",
16
+ dark: "solid-dark-purple",
17
+ };
18
+ const THEME_MODE_TOKENS = {
19
+ light: "-light-",
20
+ dark: "-dark-",
21
+ };
22
+ const THEME_MODE_SUFFIXES = {
23
+ light: "-light",
24
+ dark: "-dark",
25
+ };
26
+ function normalizeThemeKey(value) {
27
+ const normalized = value?.trim().toLowerCase();
28
+ return normalized || null;
29
+ }
30
+ function matchesThemeMode(value, mode) {
31
+ return (value.includes(THEME_MODE_TOKENS[mode])
32
+ || value.endsWith(THEME_MODE_SUFFIXES[mode])
33
+ || value === mode);
34
+ }
35
+ function titleCase(value) {
36
+ return value
37
+ .split(/[-_\s]+/)
38
+ .filter(Boolean)
39
+ .map((token) => {
40
+ if (token.toLowerCase() === "solid")
41
+ return "SolidX";
42
+ return token.charAt(0).toUpperCase() + token.slice(1);
43
+ })
44
+ .join(" ");
45
+ }
46
+ function getThemeModeFromThemeKey(value) {
47
+ const normalized = normalizeThemeKey(value);
48
+ if (!normalized)
49
+ return null;
50
+ if (matchesThemeMode(normalized, "light"))
51
+ return "light";
52
+ if (matchesThemeMode(normalized, "dark"))
53
+ return "dark";
54
+ return null;
55
+ }
56
+ function createThemeDefinition(key, defaultTheme = false) {
57
+ const mode = getThemeModeFromThemeKey(key);
58
+ if (!mode) {
59
+ throw new Error(`Invalid theme key: ${key}`);
60
+ }
61
+ return {
62
+ key,
63
+ label: titleCase(key),
64
+ mode,
65
+ default: defaultTheme,
66
+ };
67
+ }
68
+ function createDefaultTheme(mode) {
69
+ return createThemeDefinition(DEFAULT_THEME_KEYS[mode], true);
70
+ }
71
+ function groupThemesByMode(themes, mode) {
72
+ return themes.filter((theme) => theme.mode === mode);
73
+ }
74
+ function pickDefaultThemeKey(themes, mode) {
75
+ const themesForMode = groupThemesByMode(themes, mode);
76
+ return (themesForMode.find((theme) => theme.default)?.key
77
+ ?? themesForMode[0]?.key
78
+ ?? DEFAULT_THEME_KEYS[mode]);
79
+ }
80
+ function resolveThemeDirectory() {
81
+ const themeDirectory = path_1.default.resolve(process.cwd(), "..", "solid-ui", "public", "themes");
82
+ try {
83
+ if (fs_1.default.existsSync(themeDirectory) && fs_1.default.statSync(themeDirectory).isDirectory()) {
84
+ return path_1.default.resolve(themeDirectory);
85
+ }
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ return null;
91
+ }
92
+ function isThemeDirectory(themeDirectory, entryName) {
93
+ return fs_1.default.existsSync(path_1.default.join(themeDirectory, entryName, "theme.css"));
94
+ }
95
+ function readThemesFromPath() {
96
+ const themeDirectory = resolveThemeDirectory();
97
+ if (!themeDirectory) {
98
+ return [];
99
+ }
100
+ try {
101
+ const directoryEntries = fs_1.default.readdirSync(themeDirectory, { withFileTypes: true });
102
+ return directoryEntries
103
+ .filter((entry) => entry.isDirectory())
104
+ .map((entry) => entry.name.trim())
105
+ .filter(Boolean)
106
+ .filter((entryName) => isThemeDirectory(themeDirectory, entryName))
107
+ .flatMap((entryName) => {
108
+ const mode = getThemeModeFromThemeKey(entryName);
109
+ if (!mode)
110
+ return [];
111
+ return [createThemeDefinition(entryName, entryName === DEFAULT_THEME_KEYS[mode])];
112
+ })
113
+ .sort((left, right) => left.label.localeCompare(right.label));
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ }
119
+ function getThemes() {
120
+ const discoveredThemes = readThemesFromPath();
121
+ if (discoveredThemes.length) {
122
+ return discoveredThemes;
123
+ }
124
+ return [createDefaultTheme("light"), createDefaultTheme("dark")];
125
+ }
126
+ function getCoreThemes() {
127
+ return getThemes();
128
+ }
129
+ function getRegisteredThemes() {
130
+ return getThemes();
131
+ }
132
+ function getThemesByMode(mode) {
133
+ return groupThemesByMode(getRegisteredThemes(), mode);
134
+ }
135
+ function getDefaultThemeKey(mode = "light") {
136
+ return pickDefaultThemeKey(getRegisteredThemes(), mode);
137
+ }
138
+ function isRegisteredThemeKey(value, mode) {
139
+ if (!value)
140
+ return false;
141
+ return getRegisteredThemes().some((theme) => {
142
+ if (theme.key !== value)
143
+ return false;
144
+ return mode ? theme.mode === mode : true;
145
+ });
146
+ }
147
+ //# sourceMappingURL=theme-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-registry.js","sourceRoot":"","sources":["../../src/theme/theme-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\n\n/**\n * Backend theme registry.\n *\n * Responsibilities:\n * - discover theme folders from the filesystem\n * - classify each theme as light/dark from its key\n * - expose default and grouped theme helpers for backend settings\n *\n * Shared pure parsing helpers are intentionally mirrored in solid-core-ui,\n * but all filesystem discovery stays here.\n */\nexport type ThemeMode = \"light\" | \"dark\";\n\nexport type AppThemeDefinition = {\n key: string;\n label: string;\n mode: ThemeMode;\n default?: boolean;\n};\n\nconst DEFAULT_THEME_KEYS: Record<ThemeMode, string> = {\n light: \"solid-light-purple\",\n dark: \"solid-dark-purple\",\n};\n\nconst THEME_MODE_TOKENS: Record<ThemeMode, string> = {\n light: \"-light-\",\n dark: \"-dark-\",\n};\n\nconst THEME_MODE_SUFFIXES: Record<ThemeMode, string> = {\n light: \"-light\",\n dark: \"-dark\",\n};\n\nfunction normalizeThemeKey(value?: string | null): string | null {\n const normalized = value?.trim().toLowerCase();\n return normalized || null;\n}\n\nfunction matchesThemeMode(value: string, mode: ThemeMode): boolean {\n return (\n value.includes(THEME_MODE_TOKENS[mode])\n || value.endsWith(THEME_MODE_SUFFIXES[mode])\n || value === mode\n );\n}\n\nfunction titleCase(value: string): string {\n return value\n .split(/[-_\\s]+/)\n .filter(Boolean)\n .map((token) => {\n if (token.toLowerCase() === \"solid\") return \"SolidX\";\n return token.charAt(0).toUpperCase() + token.slice(1);\n })\n .join(\" \");\n}\n\nexport function getThemeModeFromThemeKey(value?: string | null): ThemeMode | null {\n const normalized = normalizeThemeKey(value);\n if (!normalized) return null;\n\n if (matchesThemeMode(normalized, \"light\")) return \"light\";\n if (matchesThemeMode(normalized, \"dark\")) return \"dark\";\n\n return null;\n}\n\nfunction createThemeDefinition(key: string, defaultTheme = false): AppThemeDefinition {\n const mode = getThemeModeFromThemeKey(key);\n if (!mode) {\n throw new Error(`Invalid theme key: ${key}`);\n }\n\n return {\n key,\n label: titleCase(key),\n mode,\n default: defaultTheme,\n };\n}\n\nfunction createDefaultTheme(mode: ThemeMode): AppThemeDefinition {\n return createThemeDefinition(DEFAULT_THEME_KEYS[mode], true);\n}\n\nfunction groupThemesByMode(themes: AppThemeDefinition[], mode: ThemeMode): AppThemeDefinition[] {\n return themes.filter((theme) => theme.mode === mode);\n}\n\nfunction pickDefaultThemeKey(themes: AppThemeDefinition[], mode: ThemeMode): string {\n const themesForMode = groupThemesByMode(themes, mode);\n\n return (\n themesForMode.find((theme) => theme.default)?.key\n ?? themesForMode[0]?.key\n ?? DEFAULT_THEME_KEYS[mode]\n );\n}\n\nfunction resolveThemeDirectory(): string | null {\n const themeDirectory = path.resolve(process.cwd(), \"..\", \"solid-ui\", \"public\", \"themes\");\n try {\n if (fs.existsSync(themeDirectory) && fs.statSync(themeDirectory).isDirectory()) {\n return path.resolve(themeDirectory);\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nfunction isThemeDirectory(themeDirectory: string, entryName: string): boolean {\n return fs.existsSync(path.join(themeDirectory, entryName, \"theme.css\"));\n}\n\nfunction readThemesFromPath(): AppThemeDefinition[] {\n const themeDirectory = resolveThemeDirectory();\n if (!themeDirectory) {\n return [];\n }\n\n try {\n const directoryEntries = fs.readdirSync(themeDirectory, { withFileTypes: true });\n\n return directoryEntries\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name.trim())\n .filter(Boolean)\n .filter((entryName) => isThemeDirectory(themeDirectory, entryName))\n .flatMap((entryName) => {\n const mode = getThemeModeFromThemeKey(entryName);\n if (!mode) return [];\n\n return [createThemeDefinition(entryName, entryName === DEFAULT_THEME_KEYS[mode])];\n })\n .sort((left, right) => left.label.localeCompare(right.label));\n } catch {\n return [];\n }\n}\n\nfunction getThemes(): AppThemeDefinition[] {\n const discoveredThemes = readThemesFromPath();\n\n if (discoveredThemes.length) {\n return discoveredThemes;\n }\n\n return [createDefaultTheme(\"light\"), createDefaultTheme(\"dark\")];\n}\n\nexport function getCoreThemes(): AppThemeDefinition[] {\n return getThemes();\n}\n\nexport function getRegisteredThemes(): AppThemeDefinition[] {\n return getThemes();\n}\n\nexport function getThemesByMode(mode: ThemeMode): AppThemeDefinition[] {\n return groupThemesByMode(getRegisteredThemes(), mode);\n}\n\nexport function getDefaultThemeKey(mode: ThemeMode = \"light\"): string {\n return pickDefaultThemeKey(getRegisteredThemes(), mode);\n}\n\nexport function isRegisteredThemeKey(value?: string | null, mode?: ThemeMode): value is string {\n if (!value) return false;\n\n return getRegisteredThemes().some((theme) => {\n if (theme.key !== value) return false;\n return mode ? theme.mode === mode : true;\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidxai/core",
3
- "version": "0.1.13-beta.12",
3
+ "version": "0.1.13-beta.14",
4
4
  "description": "This module is a NestJS module containing all the required core providers required by a Solid application",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,5 +1,7 @@
1
1
  export type MediaCategory = 'image' | 'audio' | 'video' | 'file' | 'pdf';
2
2
 
3
+ export const MEDIA_CATEGORIES = ['image', 'audio', 'video', 'file', 'pdf'] as const;
4
+
3
5
  const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff', 'heic', 'heif'];
4
6
  const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'aac', 'm4a', 'flac', 'webm'];
5
7
  const VIDEO_EXTENSIONS = ['mp4', 'mov', 'avi', 'mkv', 'mpeg', 'mpg', '3gp', '3g2', 'webm', 'ogg'];
@@ -1,5 +1,6 @@
1
1
  import { ApiProperty } from '@nestjs/swagger';
2
2
  import { Transform, Type } from 'class-transformer';
3
+ import { MEDIA_CATEGORIES, MediaCategory } from '../constants/media-file-types';
3
4
 
4
5
  import {
5
6
  IsArray,
@@ -155,13 +156,7 @@ export enum DecryptWhenType {
155
156
  afterTransit = 'after-transit',
156
157
  }
157
158
 
158
- export enum MediaType {
159
- image = 'image',
160
- audio = 'audio',
161
- video = 'video',
162
- // document = 'document',
163
- file = 'file',
164
- }
159
+ export type MediaType = MediaCategory;
165
160
 
166
161
  export enum RelationType {
167
162
  manyToOne = 'many-to-one',
@@ -308,7 +303,7 @@ export class CreateFieldMetadataDto {
308
303
  private: boolean;
309
304
 
310
305
  @ApiProperty({ description: 'Allowed media types for the field. Only for type=mediaSingle,mediaMultiple', })
311
- @IsEnum(MediaType, { each: true })
306
+ @IsIn([...MEDIA_CATEGORIES], { each: true })
312
307
  @IsOptional()
313
308
  mediaTypes: MediaType[];
314
309
 
@@ -464,4 +459,4 @@ export class CreateFieldMetadataDto {
464
459
  @ApiProperty({ description: 'Is Primary Key Field', })
465
460
  @IsBoolean()
466
461
  isPrimaryKey?: boolean;
467
- }
462
+ }
@@ -35,7 +35,7 @@ export class CreateModelSequenceDto {
35
35
  @IsOptional()
36
36
  @IsInt()
37
37
  @ApiProperty()
38
- currentValue: number;
38
+ currentValue?: number;
39
39
  @IsOptional()
40
40
  @IsString()
41
41
  @ApiProperty()
@@ -12,7 +12,12 @@ import { CommandFactory } from 'nest-commander';
12
12
  import { createWinstonLoggerConfig } from '../winston.logger';
13
13
  import { WrapResponseInterceptor } from '../interceptors/wrap-response.interceptor';
14
14
  import { buildDefaultCorsOptions } from './cors.helper';
15
- import { buildDefaultSecurityHeaderOptions, buildPermissionsPolicyHeader, PermissionsPolicyConfig } from './security.helper';
15
+ import {
16
+ buildDefaultSecurityHeaderOptions,
17
+ buildPermissionsPolicyHeader,
18
+ PermissionsPolicyConfig,
19
+ SolidSecurityOptions,
20
+ } from './security.helper';
16
21
  import { parseBooleanEnv } from './environment.helper';
17
22
 
18
23
  // ---- Shared process handlers ----
@@ -50,6 +55,8 @@ export interface SolidBootstrapOptions {
50
55
  swagger?: SolidSwaggerOptions | false;
51
56
  /** Permissions-Policy header overrides (merged with defaults). */
52
57
  permissionsPolicyOverrides?: Partial<PermissionsPolicyConfig>;
58
+ /** Security header overrides, including iframe frame-ancestor allowlists. */
59
+ security?: SolidSecurityOptions;
53
60
  /** Show full NestJS init logs during bootstrap (route mapping, module deps, pollers). Defaults to false. */
54
61
  verboseBootstrap?: boolean;
55
62
  }
@@ -64,6 +71,7 @@ export interface SolidBootstrapOptions {
64
71
  * // main.ts
65
72
  * bootstrapSolidApp(() => AppModule.forRoot(), {
66
73
  * swagger: { title: 'My API', description: 'My API description' },
74
+ * security: { frameAncestors: ["'self'", 'https://portal.example.com'] },
67
75
  * });
68
76
  */
69
77
  export async function bootstrapSolidApp(
@@ -72,7 +80,13 @@ export async function bootstrapSolidApp(
72
80
  ): Promise<void> {
73
81
  registerGlobalProcessHandlers();
74
82
 
75
- const { globalPrefix = 'api', swagger = {}, permissionsPolicyOverrides = {}, verboseBootstrap = false } = options;
83
+ const {
84
+ globalPrefix = 'api',
85
+ swagger = {},
86
+ permissionsPolicyOverrides = {},
87
+ security = {},
88
+ verboseBootstrap = false,
89
+ } = options;
76
90
 
77
91
  const startTime = Date.now();
78
92
  const appModule = await appModuleFactory();
@@ -95,7 +109,7 @@ export async function bootstrapSolidApp(
95
109
  server.get('/', (_req, res) => res.status(200).send('SOLID OK'));
96
110
 
97
111
  // Security headers
98
- app.use(helmet(buildDefaultSecurityHeaderOptions()));
112
+ app.use(helmet(buildDefaultSecurityHeaderOptions(security)));
99
113
 
100
114
  // Nest's Swagger UI HTML injects inline styles; keep CSP strict elsewhere.
101
115
  const isSwaggerPath = (path: string) =>
@@ -1,15 +1,39 @@
1
1
  import { HelmetOptions } from "helmet";
2
2
  import { Environment } from "src/decorators/disallow-in-production.decorator";
3
3
 
4
+ export interface SolidSecurityOptions {
5
+ /**
6
+ * Origins that may embed this application in an iframe.
7
+ * Defaults to the local HTTP/HTTPS loopback origins on any port.
8
+ */
9
+ frameAncestors?: string[];
10
+ }
11
+
12
+ export const DEFAULT_FRAME_ANCESTORS = [
13
+ "'self'",
14
+ "http://localhost:*",
15
+ "https://localhost:*",
16
+ "http://127.0.0.1:*",
17
+ "https://127.0.0.1:*",
18
+ "http://[::1]:*",
19
+ "https://[::1]:*",
20
+ ];
21
+
4
22
  /**
5
23
  * Default security headers for SolidX apps.
6
24
  * - HSTS only in prod over HTTPS
7
- * - CSP with frame-ancestors 'none' (prevents clickjacking)
8
- * - X-Frame-Options: DENY (legacy fallback)
25
+ * - CSP with configurable frame-ancestors
26
+ * - X-Frame-Options disabled when cross-origin frame ancestors are allowed
9
27
  * - No X-XSS-Protection (deprecated)
10
28
  */
11
- export function buildDefaultSecurityHeaderOptions(): Readonly<HelmetOptions> {
29
+ export function buildDefaultSecurityHeaderOptions(
30
+ options: SolidSecurityOptions = {},
31
+ ): Readonly<HelmetOptions> {
12
32
  const isProd = process.env.ENV === Environment.Production;
33
+ const frameAncestors = options.frameAncestors ?? DEFAULT_FRAME_ANCESTORS;
34
+ const allowsCrossOriginFrames = frameAncestors.some(
35
+ (source) => source !== "'self'" && source !== "'none'",
36
+ );
13
37
 
14
38
  return {
15
39
  // Modern CSP. Add more directives as your app needs (script-src, connect-src, etc.)
@@ -23,7 +47,7 @@ export function buildDefaultSecurityHeaderOptions(): Readonly<HelmetOptions> {
23
47
  // "form-action": ["'self'"],
24
48
 
25
49
  // clickjacking defense (modern)
26
- "frame-ancestors": ["'none'"],
50
+ "frame-ancestors": frameAncestors,
27
51
  "style-src": ["'self'"],
28
52
  // add/adjust as needed for your app:
29
53
  // "script-src": ["'self'"], // add hashes/nonces/CSPRO if needed
@@ -34,9 +58,12 @@ export function buildDefaultSecurityHeaderOptions(): Readonly<HelmetOptions> {
34
58
  },
35
59
  },
36
60
 
37
-
38
- // Legacy clickjacking defense (kept for older UAs)
39
- frameguard: { action: "deny" },
61
+ // Legacy clickjacking defense for configurations that do not allow
62
+ // cross-origin parents (kept for older UAs).
63
+ // X-Frame-Options cannot express an allowlist. It must be omitted when
64
+ // CSP allows a parent on another origin, otherwise older browsers may
65
+ // still block the iframe despite the CSP configuration.
66
+ frameguard: allowsCrossOriginFrames ? false : { action: "deny" },
40
67
 
41
68
  // Referrer/cross-origin policies
42
69
  referrerPolicy: { policy: "strict-origin-when-cross-origin" },
@@ -115,4 +142,4 @@ export function setDefaultCacheControl() {
115
142
  // res.setHeader("Permissions-Policy", buildPermissionsPolicyHeader());
116
143
  // next();
117
144
  // });
118
- // app.use(setDefaultCacheControl());
145
+ // app.use(setDefaultCacheControl());
@@ -1049,17 +1049,25 @@ export class ModuleMetadataSeederService {
1049
1049
 
1050
1050
  private async seedModelSequences(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
1051
1051
  const modelSequences = this.getSeedArray<CreateModelSequenceDto>(overallMetadata?.modelSequences);
1052
- const pruned = this.enablePruning ? await this.timeOperation('prune-model-sequences', () => this.pruneModelSequences(modelSequences, overallMetadata?.moduleMetadata?.name), {
1052
+
1053
+ // Getting rid of currentValue even if it is present in the seed data.
1054
+ // This is because we don't want to overwrite the currentValue in the database with the seed data.
1055
+ // The currentValue should be managed by the application and not by the seed data.
1056
+ const cleanModelSequences = modelSequences.map((sequence) => {
1057
+ const { currentValue, ...cleanModelSequence }: CreateModelSequenceDto = sequence;
1058
+ return cleanModelSequence;
1059
+ });
1060
+ const pruned = this.enablePruning ? await this.timeOperation('prune-model-sequences', () => this.pruneModelSequences(cleanModelSequences, overallMetadata?.moduleMetadata?.name), {
1053
1061
  moduleName: overallMetadata?.moduleMetadata?.name,
1054
1062
  component: 'model-sequences',
1055
1063
  serviceCall: 'pruneModelSequences',
1056
1064
  }) : 0;
1057
- await this.timeOperation('handle-model-sequences', () => this.handleSeedModelSequences(modelSequences), {
1065
+ await this.timeOperation('handle-model-sequences', () => this.handleSeedModelSequences(cleanModelSequences), {
1058
1066
  moduleName: overallMetadata?.moduleMetadata?.name,
1059
1067
  component: 'model-sequences',
1060
1068
  serviceCall: 'handleSeedModelSequences',
1061
1069
  });
1062
- return { pruned, upserted: modelSequences.length };
1070
+ return { pruned, upserted: cleanModelSequences.length };
1063
1071
  }
1064
1072
 
1065
1073
  // OK
@@ -12591,7 +12591,7 @@
12591
12591
  50
12592
12592
  ],
12593
12593
  "enableGlobalSearch": true,
12594
- "create": true,
12594
+ "create": false,
12595
12595
  "edit": true,
12596
12596
  "delete": true,
12597
12597
  "import": true,
@@ -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
@@ -37,6 +37,16 @@ interface ChatterMention {
37
37
  id?: string | number;
38
38
  }
39
39
 
40
+ /**
41
+ * Server-side-only overrides for `postMessage`. Not part of `PostChatterMessageDto`, so
42
+ * nothing here is settable over HTTP - only in-process callers such as migration or
43
+ * back-fill services can supply it.
44
+ */
45
+ export interface PostChatterMessageOptions {
46
+ /** Backdate the message instead of stamping it with the current time. */
47
+ createdAt?: Date;
48
+ }
49
+
40
50
  @Injectable()
41
51
  export class ChatterMessageService extends CRUDService<ChatterMessage> {
42
52
  private readonly _logger = new Logger(ChatterMessageService.name);
@@ -389,7 +399,14 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
389
399
  return this.trimMessageUser(savedMessage);
390
400
  }
391
401
 
392
- async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = []) {
402
+ /**
403
+ * Post a chatter message.
404
+ *
405
+ * `options` is deliberately kept out of `PostChatterMessageDto` so it can only be supplied
406
+ * by server-side callers (migrations, back-fills). The controller binds the DTO alone, so
407
+ * an API client has no way to backdate a message.
408
+ */
409
+ async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = [], options: PostChatterMessageOptions = {}) {
393
410
  const coModelName = lowerFirst(postDto.coModelName);
394
411
  await this.assertRecordAccess(coModelName, postDto.coModelEntityId);
395
412
 
@@ -404,6 +421,10 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
404
421
  chatterMessage.coModelEntityId = postDto.coModelEntityId;
405
422
  chatterMessage.coModelName = coModelName;
406
423
  chatterMessage.modelUserKey = postDto.modelUserKey ?? null;
424
+ // Left unset otherwise, so the @CreateDateColumn default still applies.
425
+ if (options.createdAt) {
426
+ chatterMessage.createdAt = options.createdAt;
427
+ }
407
428
 
408
429
  const model = await this.modelMetadataRepo.findOne({
409
430
  where: { singularName: coModelName },
@@ -889,25 +889,25 @@ export class CrudHelperService {
889
889
  };
890
890
  }
891
891
 
892
- async countGroupedRecords(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string) { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
893
- const { limit, offset, ...rest } = basicFilterDto;
894
- const filteredDto = { ...rest, limit: undefined, offset: undefined };
892
+ // async countGroupedRecords(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string, internationalisation?: boolean, draftPublishWorkflow?: boolean, moduleRef?: any) { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
893
+ // const { limit, offset, ...rest } = basicFilterDto;
894
+ // const filteredDto = { ...rest, limit: undefined, offset: undefined };
895
895
 
896
- const filteredQB = this.buildFilterQuery(qb, filteredDto as BasicFilterDto, entityAlias, undefined, undefined, undefined, FilterCombinator.AND, false, false);
896
+ // const filteredQB = this.buildFilterQuery(qb, filteredDto as BasicFilterDto, entityAlias, internationalisation, draftPublishWorkflow, moduleRef, FilterCombinator.AND, false, false);
897
897
 
898
- const groupByFields = this.normalize(filteredDto.groupBy);
898
+ // const groupByFields = this.normalize(filteredDto.groupBy);
899
899
 
900
- if (!groupByFields || groupByFields.length === 0) {
901
- throw new Error(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
902
- }
900
+ // if (!groupByFields || groupByFields.length === 0) {
901
+ // throw new Error(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
902
+ // }
903
903
 
904
- this.applyGroupBySelections(filteredQB, groupByFields, entityAlias);
905
- this.applyAggregates(filteredQB, ['count'], entityAlias);
906
- filteredQB.limit(undefined).offset(undefined).take(undefined).skip(undefined);
904
+ // this.applyGroupBySelections(filteredQB, groupByFields, entityAlias);
905
+ // this.applyAggregates(filteredQB, ['count'], entityAlias);
906
+ // filteredQB.limit(undefined).offset(undefined).take(undefined).skip(undefined);
907
907
 
908
- const rawResults = await filteredQB.getRawMany();
909
- return rawResults.length;
910
- }
908
+ // const rawResults = await filteredQB.getRawMany();
909
+ // return rawResults.length;
910
+ // }
911
911
 
912
912
  hasReadPermissionOnModel = (activeUser: ActiveUserData, modelName: string) => {
913
913
  const permissionNames = [`${classify(modelName)}Controller.findOne`, `${classify(modelName)}Controller.findMany`];
@@ -980,7 +980,10 @@ export class CrudHelperService {
980
980
  basicFilterDto: BasicFilterDto,
981
981
  alias: string,
982
982
  createQbFn: () => Promise<SelectQueryBuilder<T>>,
983
- postProcessEntities?: (entities: T[]) => Promise<void>
983
+ postProcessEntities?: (entities: T[]) => Promise<void>,
984
+ internationalisation?: boolean,
985
+ draftPublishWorkflow?: boolean,
986
+ moduleRef?: any
984
987
  ): Promise<{ meta: { totalRecords: number }; groupMeta: any[]; groupRecords: any[] }> {
985
988
  const groupByFields = this.normalize(basicFilterDto.groupBy);
986
989
  if (!groupByFields.length) throw new BadRequestException(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
@@ -1015,7 +1018,7 @@ export class CrudHelperService {
1015
1018
  aggregates: undefined,
1016
1019
  sort: basicFilterDto.groupFilter?.sort,
1017
1020
  };
1018
- groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias);
1021
+ groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias, internationalisation, draftPublishWorkflow, moduleRef);
1019
1022
  groupQb = this.buildGroupByRecordsQuery(groupQb, group, alias, groupAliasMap, aggregateAliasMap, groupExpressionMap);
1020
1023
  const [entities, count] = await groupQb.getManyAndCount();
1021
1024
  if (postProcessEntities) await postProcessEntities(entities);
@@ -583,7 +583,8 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
583
583
  async (entities) => {
584
584
  if (populateUserIdFields?.length) await this.handlePopulateUserIdFields(populateUserIdFields, entities);
585
585
  if (populateMedia?.length) await this.handlePopulateMedia(populateMedia, entities);
586
- }
586
+ },
587
+ internationalisation, draftPublishWorkflow, this.moduleRef
587
588
  );
588
589
  }
589
590
  else {
@@ -333,13 +333,65 @@ export class DraftPublishHelperService {
333
333
  throw new BadRequestException(`${modelName} with id ${id} is already unpublished`);
334
334
  }
335
335
 
336
- return repo.save({
337
- ...entity,
338
- isPublished: false,
339
- updatedBy: activeUser?.sub ?? entity.updatedBy,
336
+ const chainId = entity.initialEntityVersionId || entity.id;
337
+
338
+ return repo.manager.transaction(async (manager) => {
339
+ const transactionalRepo = manager.getRepository(repo.metadata.target);
340
+
341
+ // Release the "na" published tracker before anything else can claim it, mirroring
342
+ // publishRecord's archive-others-then-publish ordering in reverse. unpublishRecord
343
+ // only ever acts on the latest version (see loadLatestEntityForPublishAction), so
344
+ // this always becomes a Draft rather than an Archived version — clear publishedAt
345
+ // to match applyCreateDefaults' Draft convention instead of leaving a stale date.
346
+ const unpublishedEntity = await transactionalRepo.save({
347
+ ...entity,
348
+ isPublished: false,
349
+ publishedAt: null,
350
+ publishedTracker: this.createPublishedVersionTracker(entity.id),
351
+ updatedBy: activeUser?.sub ?? entity.updatedBy,
352
+ } as any) as unknown as T;
353
+
354
+ const chainVersions = await transactionalRepo.find({
355
+ where: [
356
+ { initialEntityVersionId: chainId },
357
+ { id: chainId },
358
+ ] as any,
359
+ }) as unknown as T[];
360
+
361
+ const versionToRestore = this.pickMostRecentlyPublishedVersion(
362
+ chainVersions.filter(version => version.id !== entity.id),
363
+ );
364
+
365
+ if (versionToRestore) {
366
+ await transactionalRepo.save({
367
+ ...versionToRestore,
368
+ isPublished: true,
369
+ publishedAt: new Date(),
370
+ publishedTracker: "na",
371
+ } as any);
372
+ }
373
+
374
+ return unpublishedEntity;
340
375
  });
341
376
  }
342
377
 
378
+ /**
379
+ * Among the other versions in a chain, find the one that was live most recently before
380
+ * being superseded (i.e. the "Archived" version unpublishRecord should restore). Ties on
381
+ * publishedAt fall back to the highest id, matching pickCurrentLocaleRecord's ordering.
382
+ */
383
+ private pickMostRecentlyPublishedVersion<T extends CommonEntity>(versions: T[]): T | null {
384
+ const previouslyPublished = versions.filter(version => Boolean(version.publishedAt));
385
+ if (previouslyPublished.length === 0) return null;
386
+
387
+ return previouslyPublished.sort((a, b) => {
388
+ const aPublishedAt = new Date(a.publishedAt as any).getTime();
389
+ const bPublishedAt = new Date(b.publishedAt as any).getTime();
390
+ if (aPublishedAt !== bPublishedAt) return bPublishedAt - aPublishedAt;
391
+ return Number(b.id) - Number(a.id);
392
+ })[0];
393
+ }
394
+
343
395
  /**
344
396
  * Load the latest version of a draft/publish-enabled record for a publish/unpublish
345
397
  * action, applying the two checks both actions share (workflow enabled, version is latest).
@@ -7,12 +7,13 @@ import { ComputedFieldMetadata, SolidRegistry } from 'src/helpers/solid-registry
7
7
  import { FieldMetadataRepository } from 'src/repository/field-metadata.repository';
8
8
  import { DataSource, Repository, SelectQueryBuilder } from 'typeorm';
9
9
  import { BasicFilterDto } from '../dtos/basic-filters.dto';
10
- import { CascadeType, ComputedFieldValueType, CreateFieldMetadataDto, DecryptWhenType, EncryptionType, MediaType, MSSQLType, PSQLType, RelationType, SelectionValueType, SolidFieldType } from '../dtos/create-field-metadata.dto';
10
+ import { CascadeType, ComputedFieldValueType, CreateFieldMetadataDto, DecryptWhenType, EncryptionType, MSSQLType, PSQLType, RelationType, SelectionValueType, SolidFieldType } from '../dtos/create-field-metadata.dto';
11
11
  import { SelectionDynamicQueryDto } from '../dtos/selection-dynamic-query.dto';
12
12
  import { UpdateFieldMetaDataDto } from '../dtos/update-field-metadata.dto';
13
13
  import { FieldMetadata } from '../entities/field-metadata.entity';
14
14
  import { ModelMetadata } from '../entities/model-metadata.entity';
15
15
  import { ISelectionProviderValues, MediaStorageProvider } from '../interfaces';
16
+ import { MEDIA_CATEGORIES } from '../constants/media-file-types';
16
17
  import { CrudHelperService } from './crud-helper.service';
17
18
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
18
19
  import qs from 'qs';
@@ -414,7 +415,7 @@ export class FieldMetadataService implements OnApplicationBootstrap {
414
415
  encryptionTypes: enumToResponseArray(EncryptionType),
415
416
  ormType: ormFieldTypeForSolid,
416
417
  decryptWhenTypes: enumToResponseArray(DecryptWhenType),
417
- mediaTypes: enumToResponseArray(MediaType),
418
+ mediaTypes: MEDIA_CATEGORIES.map((value) => ({ label: value, value })),
418
419
  relationTypes: enumToResponseArray(RelationType),
419
420
  selectionDynamicProviders: sps,
420
421
  computedProviders: cps,