@solidstarters/solid-core 1.2.206 → 1.2.207

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 (69) hide show
  1. package/dist/commands/seed.command.js +1 -1
  2. package/dist/commands/seed.command.js.map +1 -1
  3. package/dist/controllers/test.controller.d.ts.map +1 -1
  4. package/dist/controllers/test.controller.js.map +1 -1
  5. package/dist/dtos/create-scheduled-job.dto.d.ts +1 -0
  6. package/dist/dtos/create-scheduled-job.dto.d.ts.map +1 -1
  7. package/dist/dtos/create-scheduled-job.dto.js +7 -1
  8. package/dist/dtos/create-scheduled-job.dto.js.map +1 -1
  9. package/dist/dtos/update-scheduled-job.dto.d.ts +1 -0
  10. package/dist/dtos/update-scheduled-job.dto.d.ts.map +1 -1
  11. package/dist/dtos/update-scheduled-job.dto.js +7 -1
  12. package/dist/dtos/update-scheduled-job.dto.js.map +1 -1
  13. package/dist/entities/scheduled-job.entity.d.ts +1 -0
  14. package/dist/entities/scheduled-job.entity.d.ts.map +1 -1
  15. package/dist/entities/scheduled-job.entity.js +5 -1
  16. package/dist/entities/scheduled-job.entity.js.map +1 -1
  17. package/dist/helpers/command.service.d.ts +5 -1
  18. package/dist/helpers/command.service.d.ts.map +1 -1
  19. package/dist/helpers/command.service.js +26 -11
  20. package/dist/helpers/command.service.js.map +1 -1
  21. package/dist/helpers/field-crud-managers/ManyToOneRelationFieldCrudManager.js +2 -2
  22. package/dist/helpers/field-crud-managers/ManyToOneRelationFieldCrudManager.js.map +1 -1
  23. package/dist/helpers/schematic.service.d.ts +3 -2
  24. package/dist/helpers/schematic.service.d.ts.map +1 -1
  25. package/dist/helpers/schematic.service.js +32 -27
  26. package/dist/helpers/schematic.service.js.map +1 -1
  27. package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
  28. package/dist/repository/scheduled-job.repository.js +1 -0
  29. package/dist/repository/scheduled-job.repository.js.map +1 -1
  30. package/dist/seeders/module-metadata-seeder.service.js +1 -1
  31. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  32. package/dist/seeders/seed-data/solid-core-metadata.json +21 -1
  33. package/dist/seeders/system-fields-seeder.service.d.ts.map +1 -1
  34. package/dist/seeders/system-fields-seeder.service.js +3 -0
  35. package/dist/seeders/system-fields-seeder.service.js.map +1 -1
  36. package/dist/services/authentication.service.js +0 -3
  37. package/dist/services/authentication.service.js.map +1 -1
  38. package/dist/services/file.service.d.ts.map +1 -1
  39. package/dist/services/file.service.js +1 -1
  40. package/dist/services/file.service.js.map +1 -1
  41. package/dist/services/scheduled-jobs/scheduler.service.d.ts +1 -0
  42. package/dist/services/scheduled-jobs/scheduler.service.d.ts.map +1 -1
  43. package/dist/services/scheduled-jobs/scheduler.service.js +29 -0
  44. package/dist/services/scheduled-jobs/scheduler.service.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/docs/type-declaration-import-issue.md +24 -0
  47. package/package.json +2 -1
  48. package/src/commands/seed.command.ts +1 -1
  49. package/src/controllers/test.controller.ts +0 -18
  50. package/src/dtos/create-scheduled-job.dto.ts +4 -0
  51. package/src/dtos/update-scheduled-job.dto.ts +4 -0
  52. package/src/entities/scheduled-job.entity.ts +2 -0
  53. package/src/helpers/command.service.ts +44 -12
  54. package/src/helpers/field-crud-managers/ManyToOneRelationFieldCrudManager.ts +2 -2
  55. package/src/helpers/schematic.service.ts +41 -35
  56. package/src/repository/scheduled-job.repository.ts +1 -0
  57. package/src/seeders/module-metadata-seeder.service.ts +1 -1
  58. package/src/seeders/seed-data/solid-core-metadata.json +21 -1
  59. package/src/seeders/system-fields-seeder.service.ts +3 -0
  60. package/src/services/authentication.service.ts +3 -3
  61. package/src/services/file.service.ts +4 -5
  62. package/src/services/scheduled-jobs/scheduler.service.ts +40 -3
  63. package/# Password field.md +0 -8
  64. package/dist/services/file/file-service.interface.d.ts +0 -22
  65. package/dist/services/file/file-service.interface.d.ts.map +0 -1
  66. package/dist/services/file/file-service.interface.js +0 -5
  67. package/dist/services/file/file-service.interface.js.map +0 -1
  68. package/sql-server-changes.txt +0 -88
  69. package/src/services/file/file-service.interface.ts +0 -74
@@ -0,0 +1,24 @@
1
+ # Issue: Consumer sees `Property 'id' does not exist on type 'VenueUser'`
2
+
3
+ ## Summary
4
+ The published declaration files in `dist/` import internal types using `src/...` module specifiers. In a consuming project, TypeScript resolves those imports against the consumer's `src` path (or fails), which can cause `User` to extend a different `CommonEntity` that does not define `id`. This leads to `Property 'id' does not exist on type 'VenueUser'` even though `CommonEntity` in the library defines it.
5
+
6
+ ## Evidence
7
+ - `dist/entities/user.entity.d.ts` starts with:
8
+ - `import { CommonEntity } from "src/entities/common.entity";`
9
+ - `dist/entities/common.entity.d.ts` correctly contains:
10
+ - `id: number;`
11
+
12
+ Because the import is `src/...`, the consumer may resolve it to their own `src` tree instead of the library's `dist` types.
13
+
14
+ ## Root Cause
15
+ Library declarations are not portable because internal imports are using absolute `src/...` aliases. These aliases are not part of Node module resolution, so consumers resolve them differently.
16
+
17
+ ## Fix Options
18
+ 1. **Preferred:** Replace internal imports in `src/` with relative paths so emitted `.d.ts` files are portable.
19
+ 2. **Alternative:** Keep `src/*` aliases but add a post-build step (e.g., `tsc-alias`) to rewrite `dist/**/*.d.ts` imports to relative paths.
20
+
21
+ ## Next Steps
22
+ - Decide on option 1 or 2.
23
+ - Rebuild and verify `dist/**/*.d.ts` no longer import from `src/...`.
24
+ - Publish a new package version and ensure the consuming app resolves `User` and `CommonEntity` from the same package instance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidstarters/solid-core",
3
- "version": "1.2.206",
3
+ "version": "1.2.207",
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",
@@ -49,6 +49,7 @@
49
49
  "cache-manager-redis-store": "^3.0.1",
50
50
  "class-transformer": "^0.5.1",
51
51
  "class-validator": "^0.14.1",
52
+ "cron-parser": "^5.5.0",
52
53
  "dayjs": "^1.11.18",
53
54
  "exceljs": "^4.4.0",
54
55
  "fast-csv": "^5.0.2",
@@ -42,7 +42,7 @@ export class SeedCommand extends CommandRunner {
42
42
  this.logger.error(`Seeder service ${options.seeder} not found. Does your service have a seed() method?`);
43
43
  return;
44
44
  }
45
- this.logger.log(`Running the seed() method for seeder :${seeder.constructor.name}`);
45
+ this.logger.log(`Running the seed() method for seeder: ${seeder.constructor.name}`);
46
46
  await seeder.seed(parsedConf);
47
47
  }
48
48
 
@@ -44,22 +44,4 @@ export class TestController {
44
44
  await this.ingestMetadataService.ingest();
45
45
  return { ok: true };
46
46
  }
47
-
48
- // @Public()
49
- // @Post('seed')
50
- // async seedData(@Body() seedData: any) {
51
- // const seeder = this.solidRegistry
52
- // .getSeeders()
53
- // .filter((seeder) => seeder.name === seedData.seeder)
54
- // .map((seeder) => seeder.instance)
55
- // .pop();
56
- // if (!seeder) {
57
- // this.logger.error(`Seeder service ${seedData.seeder} not found. Does your service have a seed() method?`);
58
- // return;
59
- // }
60
- // this.logger.log(`Running the seed() method for seeder :${seeder.constructor.name}`);
61
- // await seeder.seed();
62
- // return { message: `seed data for ${seedData.seeder}` };
63
- // }
64
-
65
47
  }
@@ -52,6 +52,10 @@ export class CreateScheduledJobDto {
52
52
  @ApiProperty()
53
53
  job: string;
54
54
  @IsOptional()
55
+ @IsString()
56
+ @ApiProperty()
57
+ cronExpression: string;
58
+ @IsOptional()
55
59
  @IsInt()
56
60
  @ApiProperty()
57
61
  moduleId: number;
@@ -58,6 +58,10 @@ export class UpdateScheduledJobDto {
58
58
  @ApiProperty()
59
59
  job: string;
60
60
  @IsOptional()
61
+ @IsString()
62
+ @ApiProperty()
63
+ cronExpression: string;
64
+ @IsOptional()
61
65
  @IsInt()
62
66
  @ApiProperty()
63
67
  moduleId: number;
@@ -29,6 +29,8 @@ export class ScheduledJob extends CommonEntity {
29
29
  dayOfWeek: string;
30
30
  @Column({ type: "varchar" })
31
31
  job: string;
32
+ @Column({ type: "varchar", nullable: true })
33
+ cronExpression: string;
32
34
  @Index()
33
35
  @ManyToOne(() => ModuleMetadata, { nullable: false })
34
36
  @JoinColumn({ referencedColumnName: 'id' })
@@ -1,22 +1,54 @@
1
1
  import { Injectable, Logger } from '@nestjs/common';
2
- import { exec } from 'child_process';
2
+ import { spawn } from 'child_process';
3
+
4
+ export type CommandWithArgs = {
5
+ command: string;
6
+ args: string[];
7
+ };
3
8
 
4
9
  @Injectable()
5
10
  export class CommandService {
6
11
  private readonly logger = new Logger(CommandService.name);
7
12
 
8
- async executeCommand(command: string): Promise<string> {
9
- this.logger.debug(`Executing command :${command}`);
13
+ /**
14
+ * Execute a command with arguments array (cross-platform compatible)
15
+ */
16
+ async executeCommandWithArgs(commandWithArgs: CommandWithArgs): Promise<string> {
17
+ const { command, args } = commandWithArgs;
18
+ this.logger.debug(`Executing command: ${command} ${args.join(' ')}`);
19
+
10
20
  return new Promise<string>((resolve, reject) => {
11
- exec(command, (error, stdout, stderr) => {
12
- if (error) {
13
- this.logger.error(`Error executing command :${command}`, error);
14
- reject(error);
15
- return;
16
- }
17
- if (stderr) {
18
- this.logger.error(`Error executing command :${command}`, stderr);
19
- reject(stderr);
21
+ const isWindows = process.platform === 'win32';
22
+
23
+ // On Windows, we need to use cmd /c for commands that might be .cmd files (like npm scripts)
24
+ const spawnCommand = isWindows ? command : command;
25
+ const spawnArgs = args;
26
+
27
+ const child = spawn(spawnCommand, spawnArgs, {
28
+ shell: isWindows, // Use shell on Windows to handle .cmd files
29
+ stdio: ['pipe', 'pipe', 'pipe'],
30
+ });
31
+
32
+ let stdout = '';
33
+ let stderr = '';
34
+
35
+ child.stdout.on('data', (data) => {
36
+ stdout += data.toString();
37
+ });
38
+
39
+ child.stderr.on('data', (data) => {
40
+ stderr += data.toString();
41
+ });
42
+
43
+ child.on('error', (error) => {
44
+ this.logger.error(`Error executing command: ${command}`, error);
45
+ reject(error);
46
+ });
47
+
48
+ child.on('close', (code) => {
49
+ if (code !== 0) {
50
+ this.logger.error(`Command failed with code ${code}: ${command}`, stderr);
51
+ reject(new Error(stderr || `Command failed with exit code ${code}`));
20
52
  return;
21
53
  }
22
54
  resolve(stdout);
@@ -77,13 +77,13 @@ export class ManyToOneRelationFieldCrudManager implements FieldCrudManager {
77
77
  if (isNotEmpty(fieldId)) {
78
78
  dto[this.options.fieldName] = await this.options.entityManager.getRepository(coModelEntityName).findOneBy({ id: fieldId });
79
79
  if (this.options.required && isEmpty(dto[this.options.fieldName])) {
80
- throw new Error(`ManyToOneRelationFieldCrudManager: Record with id: ${fieldId} not found in ${this.options.relationCoModelSingularName}`);
80
+ throw new Error(`Field ${this.options.fieldName} resolution failed. Record with id: ${fieldId} not found in ${this.options.relationCoModelSingularName}`);
81
81
  }
82
82
  }
83
83
  else {
84
84
  dto[this.options.fieldName] = await this.options.entityManager.getRepository(coModelEntityName).findOneBy({ [this.options.relationCoModelUserKeyFieldName]: fieldUserKeyValue });
85
85
  if (this.options.required && isEmpty(dto[this.options.fieldName])) {
86
- throw new Error(`ManyToOneRelationFieldCrudManager: Record with userKey: ${this.options.relationCoModelUserKeyFieldName}: ${fieldUserKeyValue} not found in ${this.options.relationCoModelSingularName}`);
86
+ throw new Error(`Field ${this.options.fieldName} resolution failed. Record with userKey: ${this.options.relationCoModelUserKeyFieldName}: ${fieldUserKeyValue} not found in ${this.options.relationCoModelSingularName}`);
87
87
  }
88
88
  }
89
89
 
@@ -1,7 +1,7 @@
1
1
  import { Injectable, Logger } from '@nestjs/common';
2
2
  import { snakeCase } from "lodash";
3
3
  import { FieldMetadata } from 'src/entities/field-metadata.entity';
4
- import { CommandService } from './command.service';
4
+ import { CommandService, CommandWithArgs } from './command.service';
5
5
  import { SolidRegistry } from './solid-registry';
6
6
 
7
7
  export const ADD_MODULE_COMMAND = 'add-module';
@@ -17,7 +17,7 @@ type ModelAndFieldGenerationOptions = {
17
17
  table?: string;
18
18
  dataSource: string;
19
19
  modelEnableSoftDelete?: boolean;
20
- parentModel?: string;
20
+ parentModel?: string;
21
21
  parentModule?: string;
22
22
  draftPublishWorkflowEnabled?: boolean;
23
23
  isLegacyTable?: boolean;
@@ -33,6 +33,8 @@ export const REFRESH_MODEL_COMMAND = 'refresh-model';
33
33
  export class SchematicService {
34
34
  private readonly logger = new Logger(SchematicService.name);
35
35
  private readonly SCHEMATIC_PROJECT = '@solidstarters/solid-code-builder';
36
+ private readonly SCHEMATICS_COMMAND = 'schematics';
37
+
36
38
  constructor(private readonly commandService: CommandService, private readonly solidRegistry: SolidRegistry) { }
37
39
 
38
40
  async executeSchematicCommand(
@@ -40,91 +42,95 @@ export class SchematicService {
40
42
  options: GenerateModuleOptions | ModelAndFieldGenerationOptions,
41
43
  debug = false,
42
44
  ): Promise<string> {
43
- return await this.commandService.executeCommand(
44
- this.generateSchematicCommand(command, options, debug),
45
- );
45
+ const commandWithArgs = this.generateSchematicCommand(command, options, debug);
46
+ return await this.commandService.executeCommandWithArgs(commandWithArgs);
46
47
  }
47
48
 
48
49
  private generateSchematicCommand(
49
50
  command: string,
50
51
  options: GenerateModuleOptions | ModelAndFieldGenerationOptions,
51
52
  debug: boolean,
52
- ): string {
53
- const baseCommand = `schematics ${this.SCHEMATIC_PROJECT}:${command} --debug=${debug}`;
53
+ ): CommandWithArgs {
54
+ const schematicName = `${this.SCHEMATIC_PROJECT}:${command}`;
55
+ const baseArgs = [schematicName, `--debug=${debug}`];
56
+
54
57
  if (
55
58
  command === REMOVE_FIELDS_COMMAND ||
56
59
  command === REFRESH_MODEL_COMMAND
57
60
  ) {
58
- const {fields, ...modelSpecificOptions} = options as ModelAndFieldGenerationOptions;
59
- const modelCommand = this.buildModelGenerationCommand(baseCommand, modelSpecificOptions);
60
- const fieldCommand = this.buildFieldGenerationCommand(fields);
61
- const schematicCommand = modelCommand + ' ' + fieldCommand;
62
- this.logger.debug('schematicCommand', schematicCommand);
63
- return schematicCommand;
61
+ const { fields, ...modelSpecificOptions } = options as ModelAndFieldGenerationOptions;
62
+ const modelArgs = this.buildModelGenerationArgs(modelSpecificOptions);
63
+ const fieldArgs = this.buildFieldGenerationArgs(fields);
64
+ const args = [...baseArgs, ...modelArgs, ...fieldArgs];
65
+ this.logger.debug('schematicCommand args', args);
66
+ return { command: this.SCHEMATICS_COMMAND, args };
64
67
  } else if (command === ADD_MODULE_COMMAND) {
65
68
  const moduleOptions = options as GenerateModuleOptions;
66
- const schematicCommand = ` ${baseCommand} --module=${moduleOptions.module}`;
67
- this.logger.debug('schematicCommand', schematicCommand);
68
- return schematicCommand;
69
+ const args = [...baseArgs, `--module=${moduleOptions.module}`];
70
+ this.logger.debug('schematicCommand args', args);
71
+ return { command: this.SCHEMATICS_COMMAND, args };
69
72
  } else {
70
73
  throw new Error('Schematic command not supported.');
71
74
  }
72
75
  }
73
76
 
74
- private buildFieldGenerationCommand(fields: FieldMetadata[]) {
77
+ private buildFieldGenerationArgs(fields: FieldMetadata[]): string[] {
75
78
  return fields
76
79
  .filter((field) => {
77
80
  return !this.solidRegistry.getCommonEntityKeys().map(key => key.toString()).includes(field.name);
78
81
  })
79
82
  .map((field) => {
80
- return `--fields='${JSON.stringify(field).replace(/'/g, "\\'")}'`;
81
- })
82
- .join(' ');
83
+ // Using argument array eliminates the need for shell-specific quoting
84
+ return `--fields=${JSON.stringify(field)}`;
85
+ });
83
86
  }
84
87
 
85
- private buildModelGenerationCommand(baseCommand: string, modelSpecificOptions: ModelGenerationOptions): string {
86
- let modelCommand = `${baseCommand} --module=${modelSpecificOptions.module} --model=${modelSpecificOptions.model}`;
88
+ private buildModelGenerationArgs(modelSpecificOptions: ModelGenerationOptions): string[] {
89
+ const args: string[] = [
90
+ `--module=${modelSpecificOptions.module}`,
91
+ `--model=${modelSpecificOptions.model}`,
92
+ ];
87
93
 
88
- // Make below options code generate i.e if option exists then add to command with proper casing
89
94
  if (modelSpecificOptions.moduleDisplayName) {
90
- modelCommand += ` --module-display-name=${snakeCase(modelSpecificOptions.moduleDisplayName)}`;
95
+ args.push(`--module-display-name=${snakeCase(modelSpecificOptions.moduleDisplayName)}`);
91
96
  }
92
97
 
93
98
  if (modelSpecificOptions.table) {
94
- modelCommand += ` --table=${modelSpecificOptions.table}`;
99
+ args.push(`--table=${modelSpecificOptions.table}`);
95
100
  }
96
101
 
97
102
  if (modelSpecificOptions.dataSource) {
98
- modelCommand += ` --data-source=${modelSpecificOptions.dataSource}`;
103
+ args.push(`--data-source=${modelSpecificOptions.dataSource}`);
99
104
  }
100
105
 
101
106
  if (modelSpecificOptions.modelEnableSoftDelete) {
102
- modelCommand += ` --model-enable-soft-delete=${modelSpecificOptions.modelEnableSoftDelete}`;
107
+ args.push(`--model-enable-soft-delete=${modelSpecificOptions.modelEnableSoftDelete}`);
103
108
  }
104
109
 
105
110
  if (modelSpecificOptions.parentModel) {
106
- modelCommand += ` --parent-model=${modelSpecificOptions.parentModel}`;
111
+ args.push(`--parent-model=${modelSpecificOptions.parentModel}`);
107
112
  }
113
+
108
114
  if (modelSpecificOptions.parentModule) {
109
- modelCommand += ` --parent-module=${modelSpecificOptions.parentModule}`;
115
+ args.push(`--parent-module=${modelSpecificOptions.parentModule}`);
110
116
  }
111
117
 
112
118
  if (modelSpecificOptions.draftPublishWorkflowEnabled) {
113
- modelCommand += ` --draft-publish-workflow-enabled=${modelSpecificOptions.draftPublishWorkflowEnabled}`;
119
+ args.push(`--draft-publish-workflow-enabled=${modelSpecificOptions.draftPublishWorkflowEnabled}`);
114
120
  }
115
121
 
116
122
  if (modelSpecificOptions.isLegacyTable) {
117
- modelCommand += ` --is-legacy-table=${modelSpecificOptions.isLegacyTable}`;
123
+ args.push(`--is-legacy-table=${modelSpecificOptions.isLegacyTable}`);
118
124
  }
119
125
 
120
126
  if (modelSpecificOptions.isLegacyTableWithId) {
121
- modelCommand += ` --is-legacy-table-with-id=${modelSpecificOptions.isLegacyTableWithId}`;
127
+ args.push(`--is-legacy-table-with-id=${modelSpecificOptions.isLegacyTableWithId}`);
122
128
  }
123
129
 
124
130
  if (modelSpecificOptions.dataSourceType) {
125
- modelCommand += ` --data-source-type=${modelSpecificOptions.dataSourceType}`;
131
+ args.push(`--data-source-type=${modelSpecificOptions.dataSourceType}`);
126
132
  }
127
-
128
- return modelCommand;
133
+
134
+ return args;
129
135
  }
130
136
  }
@@ -70,6 +70,7 @@ export class ScheduledJobRepository extends SolidBaseRepository<ScheduledJob> {
70
70
  job: populatedScheduledJob.job,
71
71
  moduleUserKey: populatedScheduledJob.module.name,
72
72
  moduleId: populatedScheduledJob.module.id,
73
+ cronExpression: populatedScheduledJob.cronExpression,
73
74
  };
74
75
  }
75
76
 
@@ -88,7 +88,7 @@ export class ModuleMetadataSeederService {
88
88
  // Module specific seeding steps.
89
89
  // Get all the module metadata files which needs to be seeded.
90
90
  const seedDataFiles = this.seedDataFiles;
91
- this.logger.debug(`Seed data files are: ${seedDataFiles}`);
91
+ this.logger.debug(`Found seed data for modules: ${seedDataFiles.map(s => s.moduleMetadata?.name)}`);
92
92
 
93
93
  /** -------------------------------------------------------------
94
94
  * Selective module seeding via:
@@ -2613,7 +2613,8 @@
2613
2613
  "Hourly:Hourly",
2614
2614
  "Daily:Daily",
2615
2615
  "Weekly:Weekly",
2616
- "Monthly:Monthly"
2616
+ "Monthly:Monthly",
2617
+ "Custom:Custom"
2617
2618
  ],
2618
2619
  "selectionValueType": "string",
2619
2620
  "required": true,
@@ -2787,6 +2788,19 @@
2787
2788
  "selectionDynamicProvider": "ListOfScheduledJobsSelectionProvider",
2788
2789
  "selectionDynamicProviderCtxt": "{}"
2789
2790
  },
2791
+ {
2792
+ "name": "cronExpression",
2793
+ "displayName": "Cron Expression",
2794
+ "type": "shortText",
2795
+ "ormType": "varchar",
2796
+ "length": 512,
2797
+ "required": false,
2798
+ "unique": false,
2799
+ "index": false,
2800
+ "private": false,
2801
+ "encrypt": false,
2802
+ "isSystem": false
2803
+ },
2790
2804
  {
2791
2805
  "name": "module",
2792
2806
  "displayName": "Module",
@@ -10306,6 +10320,12 @@
10306
10320
  "name": "frequency"
10307
10321
  }
10308
10322
  },
10323
+ {
10324
+ "type": "field",
10325
+ "attrs": {
10326
+ "name": "cronExpression"
10327
+ }
10328
+ },
10309
10329
  {
10310
10330
  "type": "field",
10311
10331
  "attrs": {
@@ -53,5 +53,8 @@ export class SystemFieldsSeederService {
53
53
  }));
54
54
  await this.fieldMetadataRepository.save(newFields);
55
55
  }
56
+ else {
57
+ this.logger.debug(`No missing system fields for model: ${model.singularName}`);
58
+ }
56
59
  }
57
60
  }
@@ -155,9 +155,9 @@ export class AuthenticationService {
155
155
 
156
156
  async signUp(signUpDto: SignUpDto, activeUser: ActiveUserData = null): Promise<User> {
157
157
  // If public registrations are disabled and no activeUser is present when invoking signUp then we throw an exception.
158
- if (!(this.settingService.getConfigValue<SolidCoreSetting>('allowPublicRegistration')) && !activeUser) {
159
- throw new BadRequestException(ERROR_MESSAGES.PUBLIC_REGISTRATION_DISABLED);
160
- }
158
+ // if (!(this.settingService.getConfigValue<SolidCoreSetting>('allowPublicRegistration')) && !activeUser) {
159
+ // throw new BadRequestException(ERROR_MESSAGES.PUBLIC_REGISTRATION_DISABLED);
160
+ // }
161
161
 
162
162
  try {
163
163
  const onForcePasswordChange = this.settingService.getConfigValue<SolidCoreSetting>('forceChangePasswordOnFirstLogin');
@@ -1,13 +1,12 @@
1
- import { Inject, Injectable, Logger } from '@nestjs/common';
1
+ import { Injectable, Logger } from '@nestjs/common';
2
2
  import * as fs from 'fs';
3
3
  // import * as AWS from 'aws-sdk';
4
- import { S3Client, PutObjectCommand, DeleteObjectCommand, ObjectCannedACL, GetObjectCommand } from '@aws-sdk/client-s3';
5
- import { ConfigType } from '@nestjs/config';
6
- import path from 'path';
7
- import { Readable } from 'stream';
4
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
8
5
  import { getSignedUrl as awsGetSignedUrl } from '@aws-sdk/s3-request-presigner';
6
+ import path from 'path';
9
7
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
10
8
  import { AwsS3Config } from 'src/interfaces';
9
+ import { Readable } from 'stream';
11
10
 
12
11
  @Injectable()
13
12
  export class FileService {
@@ -6,6 +6,7 @@ import { ScheduledJob } from 'src/entities/scheduled-job.entity';
6
6
  import { SolidRegistry } from 'src/helpers/solid-registry';
7
7
  import { ScheduledJobRepository } from 'src/repository/scheduled-job.repository';
8
8
  import { ISchedulerService } from './scheduler.interface';
9
+ import { CronExpressionParser } from 'cron-parser';
9
10
 
10
11
  @Injectable()
11
12
  export class SchedulerServiceImpl implements ISchedulerService {
@@ -93,6 +94,13 @@ export class SchedulerServiceImpl implements ISchedulerService {
93
94
  if (timeNow > jobEnd) return false;
94
95
  }
95
96
 
97
+ // 3. Check custom frequency
98
+ if (job.frequency.toLowerCase() === 'custom') {
99
+ // Custom cron expressions handle their own scheduling logic
100
+ // Just check if nextRunAt is due, which was already checked in the query
101
+ return true;
102
+ }
103
+
96
104
  // 3. Check dayOfWeek (for weekly)
97
105
  if (job.frequency.toLowerCase() === 'weekly' && job.dayOfWeek) {
98
106
  const todayName = now.toLocaleString('en-US', { weekday: 'long' }); // e.g., "Monday"
@@ -110,6 +118,36 @@ export class SchedulerServiceImpl implements ISchedulerService {
110
118
  return true;
111
119
  }
112
120
 
121
+ private computeNextRunForCustomCron(job: ScheduledJob, from: Date): Date {
122
+ const base = new Date(from);
123
+
124
+ if (!job.cronExpression) {
125
+ this.logger.error(`Custom frequency requires cronExpression for job ${job.scheduleName}`);
126
+ // Fallback to daily if cron expression is missing
127
+ return new Date(base.getTime() + 24 * 60 * 60 * 1000);
128
+ }
129
+
130
+ try {
131
+ const interval = CronExpressionParser.parse(job.cronExpression, {
132
+ currentDate: from,
133
+ tz: 'UTC'
134
+ });
135
+ const nextRun = interval.next().toDate();
136
+
137
+ // Validate minimum 1 minute interval
138
+ if (nextRun.getTime() - from.getTime() < 60000) {
139
+ throw new Error('Cron expression interval must be at least 1 minute');
140
+ }
141
+
142
+ this.logger.log(`Custom cron '${job.cronExpression}' next run: ${nextRun}`);
143
+ return nextRun;
144
+ } catch (error) {
145
+ this.logger.error(`Invalid cron expression for job ${job.scheduleName}: ${job.cronExpression}`, error);
146
+ // Fallback to daily if cron parsing fails
147
+ return new Date(base.getTime() + 24 * 60 * 60 * 1000);
148
+ }
149
+ }
150
+
113
151
  private computeNextRunAt(job: ScheduledJob, from: Date): Date {
114
152
  const base = new Date(from);
115
153
 
@@ -128,9 +166,8 @@ export class SchedulerServiceImpl implements ISchedulerService {
128
166
  const next = new Date(base);
129
167
  next.setMonth(base.getMonth() + 1);
130
168
  return next;
131
- // case 'custom':
132
- // // Optional: let job handler decide via metadata or registry
133
- // return new Date(base.getTime() + 24 * 60 * 60 * 1000);
169
+ case 'custom':
170
+ return this.computeNextRunForCustomCron(job, from);
134
171
  default:
135
172
  return new Date(base.getTime() + 24 * 60 * 60 * 1000);
136
173
  }
@@ -1,8 +0,0 @@
1
- # Password field
2
- ## Create Scenario
3
- - password
4
- - passwordConfirm (stuck)
5
- ## Update Scenario
6
- - A button will show up instead & password fields won't be shown in the form
7
- - Clicking on the button will open a modal containing the password & passwordConfirm fields, with a button named as "Update". clicking on Update will patch the record with the password & passwordConfirm fields
8
- - Add an extra in validateAndTranform in crud service i.e isUpdate i.e default false. this will true when update method of crud service is called. Add logic in PasswordFieldCrudManager to throw required error only if !isUpdate
@@ -1,22 +0,0 @@
1
- import { Readable } from 'stream';
2
- export interface WriteOptions {
3
- contentType?: string;
4
- }
5
- export interface CopyOptions {
6
- contentType?: string;
7
- overwrite?: boolean;
8
- }
9
- export interface UrlOptions {
10
- expiresIn?: number;
11
- }
12
- export interface IFileService {
13
- read(path: string): Promise<Buffer>;
14
- write(path: string, data: Buffer | string, options?: WriteOptions): Promise<void>;
15
- writeStream(path: string, stream: Readable, options?: WriteOptions): Promise<void>;
16
- delete(path: string): Promise<void>;
17
- exists(path: string): Promise<boolean>;
18
- copy(sourcePath: string, destinationPath: string, options?: CopyOptions): Promise<void>;
19
- getUrl(path: string, options?: UrlOptions): Promise<string>;
20
- }
21
- export declare const FILE_SERVICE: unique symbol;
22
- //# sourceMappingURL=file-service.interface.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"file-service.interface.d.ts","sourceRoot":"","sources":["../../../src/services/file/file-service.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAEzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,MAAM,WAAW,YAAY;IAK3B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAQpC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAQlF,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAMnF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAMpC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAQvC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAQxF,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7D;AAED,eAAO,MAAM,YAAY,eAAyB,CAAC"}
@@ -1,5 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FILE_SERVICE = void 0;
4
- exports.FILE_SERVICE = Symbol('FILE_SERVICE');
5
- //# sourceMappingURL=file-service.interface.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"file-service.interface.js","sourceRoot":"","sources":["../../../src/services/file/file-service.interface.ts"],"names":[],"mappings":";;;AAyEa,QAAA,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC","sourcesContent":["import { Readable } from 'stream';\n\nexport interface WriteOptions {\n contentType?: string;\n}\n\nexport interface CopyOptions {\n contentType?: string;\n /** Whether to overwrite existing file (default: true) */\n overwrite?: boolean;\n}\n\nexport interface UrlOptions {\n /** Expiry time in seconds (relevant for S3 signed URLs) */\n expiresIn?: number;\n}\n\n/**\n * Unified interface for file operations.\n * Implementations: DiskFileService, S3FileService\n */\nexport interface IFileService {\n /**\n * Read file contents as Buffer\n * @param path - File path (disk) or key (S3)\n */\n read(path: string): Promise<Buffer>;\n\n /**\n * Write data to a file\n * @param path - File path (disk) or key (S3)\n * @param data - Content to write\n * @param options - Optional write options (e.g., contentType for S3)\n */\n write(path: string, data: Buffer | string, options?: WriteOptions): Promise<void>;\n\n /**\n * Write a stream to a file\n * @param path - File path (disk) or key (S3)\n * @param stream - Readable stream\n * @param options - Optional write options (e.g., contentType for S3)\n */\n writeStream(path: string, stream: Readable, options?: WriteOptions): Promise<void>;\n\n /**\n * Delete a file\n * @param path - File path (disk) or key (S3)\n */\n delete(path: string): Promise<void>;\n\n /**\n * Check if file exists\n * @param path - File path (disk) or key (S3)\n */\n exists(path: string): Promise<boolean>;\n\n /**\n * Copy a file from source to destination\n * @param sourcePath - Source file path\n * @param destinationPath - Destination file path\n * @param options - Optional copy options\n */\n copy(sourcePath: string, destinationPath: string, options?: CopyOptions): Promise<void>;\n\n /**\n * Get an accessible URL for the file\n * For S3: returns signed URL; For disk: returns the file path\n * @param path - File path (disk) or key (S3)\n * @param options - URL options (e.g., expiresIn for S3)\n */\n getUrl(path: string, options?: UrlOptions): Promise<string>;\n}\n\nexport const FILE_SERVICE = Symbol('FILE_SERVICE');\n"]}