@solidxai/core 0.1.11-beta.0 → 0.1.11-beta.1

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 (51) hide show
  1. package/dist/repository/scheduled-job.repository.d.ts +3 -0
  2. package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
  3. package/dist/repository/scheduled-job.repository.js +60 -0
  4. package/dist/repository/scheduled-job.repository.js.map +1 -1
  5. package/dist/seeders/module-metadata-seeder.service.d.ts +32 -1
  6. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  7. package/dist/seeders/module-metadata-seeder.service.js +1011 -230
  8. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  9. package/dist/services/action-metadata.service.d.ts +7 -0
  10. package/dist/services/action-metadata.service.d.ts.map +1 -1
  11. package/dist/services/action-metadata.service.js +76 -3
  12. package/dist/services/action-metadata.service.js.map +1 -1
  13. package/dist/services/role-metadata.service.d.ts +4 -0
  14. package/dist/services/role-metadata.service.d.ts.map +1 -1
  15. package/dist/services/role-metadata.service.js +72 -31
  16. package/dist/services/role-metadata.service.js.map +1 -1
  17. package/dist/services/view-metadata.service.d.ts +6 -0
  18. package/dist/services/view-metadata.service.d.ts.map +1 -1
  19. package/dist/services/view-metadata.service.js +66 -1
  20. package/dist/services/view-metadata.service.js.map +1 -1
  21. package/dist-tests/api/authenticate.spec.js +119 -0
  22. package/dist-tests/api/authenticate.spec.js.map +1 -0
  23. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
  24. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
  25. package/dist-tests/api/ping.spec.js +21 -0
  26. package/dist-tests/api/ping.spec.js.map +1 -0
  27. package/dist-tests/helpers/auth.js +41 -0
  28. package/dist-tests/helpers/auth.js.map +1 -0
  29. package/dist-tests/helpers/env.js +11 -0
  30. package/dist-tests/helpers/env.js.map +1 -0
  31. package/docs/agent-hub-grooming.md +301 -0
  32. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +438 -0
  33. package/docs/dashboards/dashboard-curl-smoke-tests.txt +146 -0
  34. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +172 -0
  35. package/docs/datasource-introspection-ddl-analysis.md +326 -0
  36. package/docs/datasource-introspection-implementation-plan.md +306 -0
  37. package/docs/grouping-enhancements.md +89 -0
  38. package/docs/java-spring/README.md +3 -0
  39. package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
  40. package/docs/module-package-import-handoff.md +691 -0
  41. package/docs/seed-changes.md +65 -0
  42. package/docs/test-data-workflow.md +200 -0
  43. package/docs/type-declaration-import-issue.md +24 -0
  44. package/package.json +1 -1
  45. package/src/repository/scheduled-job.repository.ts +73 -0
  46. package/src/seeders/module-metadata-seeder.service.ts +1223 -262
  47. package/src/services/action-metadata.service.ts +87 -2
  48. package/src/services/role-metadata.service.ts +86 -47
  49. package/src/services/view-metadata.service.ts +79 -1
  50. package/.claude/settings.local.json +0 -15
  51. package/src/services/1.js +0 -6
@@ -1,4 +1,4 @@
1
- import { Injectable } from '@nestjs/common';
1
+ import { Injectable, Logger } from '@nestjs/common';
2
2
  import { ModuleRef } from "@nestjs/core";
3
3
  import { InjectEntityManager } from '@nestjs/typeorm';
4
4
  import { CRUDService } from 'src/services/crud.service';
@@ -10,6 +10,79 @@ import { ActionMetadata } from '../entities/action-metadata.entity';
10
10
 
11
11
  @Injectable()
12
12
  export class ActionMetadataService extends CRUDService<ActionMetadata> {
13
+ private readonly logger = new Logger(ActionMetadataService.name);
14
+ private readonly comparableFields = [
15
+ 'name',
16
+ 'displayName',
17
+ 'type',
18
+ 'domain',
19
+ 'context',
20
+ 'customComponent',
21
+ 'customIsModal',
22
+ 'serverEndpoint',
23
+ 'module',
24
+ 'model',
25
+ 'view',
26
+ ] as const;
27
+ private readonly relationCompareFields = new Set(['module', 'model', 'view']);
28
+ private readonly jsonCompareFields = new Set(['domain', 'context']);
29
+
30
+ private normalizeJsonFieldValue(value: any): any {
31
+ if (typeof value === 'string') {
32
+ const trimmedValue = value.trim();
33
+ if (
34
+ (trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
35
+ (trimmedValue.startsWith('[') && trimmedValue.endsWith(']'))
36
+ ) {
37
+ try {
38
+ return this.normalizeJsonFieldValue(JSON.parse(trimmedValue));
39
+ } catch {
40
+ return value;
41
+ }
42
+ }
43
+ return value;
44
+ }
45
+
46
+ if (Array.isArray(value)) {
47
+ return value.map((item) => this.normalizeJsonFieldValue(item));
48
+ }
49
+
50
+ if (value && typeof value === 'object') {
51
+ return Object.keys(value)
52
+ .sort()
53
+ .reduce((result, key) => {
54
+ result[key] = this.normalizeJsonFieldValue(value[key]);
55
+ return result;
56
+ }, {} as Record<string, any>);
57
+ }
58
+
59
+ return value;
60
+ }
61
+
62
+ private getCanonicalJsonFieldString(value: any): string {
63
+ return JSON.stringify(this.normalizeJsonFieldValue(value) ?? null);
64
+ }
65
+
66
+ private hasChanges(existingSolidAction: ActionMetadata, updateSolidActionDto: any): boolean {
67
+ return this.comparableFields.some((key) => {
68
+ const value = updateSolidActionDto[key];
69
+ if (typeof value === 'undefined') {
70
+ return false;
71
+ }
72
+
73
+ if (this.relationCompareFields.has(key)) {
74
+ const relationValue = value as any;
75
+ return (existingSolidAction as any)[key]?.id !== relationValue?.id;
76
+ }
77
+
78
+ if (this.jsonCompareFields.has(key)) {
79
+ return this.getCanonicalJsonFieldString((existingSolidAction as any)[key]) !== this.getCanonicalJsonFieldString(value);
80
+ }
81
+
82
+ return (existingSolidAction as any)[key] !== value;
83
+ });
84
+ }
85
+
13
86
  constructor(
14
87
  @InjectEntityManager()
15
88
  readonly entityManager: EntityManager,
@@ -37,11 +110,23 @@ export class ActionMetadataService extends CRUDService<ActionMetadata> {
37
110
  const existingSolidAction = await this.repo.findOne({
38
111
  where: {
39
112
  name: updateSolidActionDto.name
40
- }
113
+ },
114
+ relations: {
115
+ module: true,
116
+ model: true,
117
+ view: true,
118
+ },
41
119
  })
42
120
 
43
121
  // if found
44
122
  if (existingSolidAction) {
123
+ const hasChanges = this.hasChanges(existingSolidAction, updateSolidActionDto);
124
+
125
+ if (!hasChanges) {
126
+ this.logger.debug(`Skipping action upsert for ${updateSolidActionDto.name}; no changes detected.`);
127
+ return existingSolidAction;
128
+ }
129
+
45
130
  const updatedSolidActionDto = { ...existingSolidAction, ...updateSolidActionDto };
46
131
  return this.repo.save(updatedSolidActionDto);
47
132
  }
@@ -14,6 +14,7 @@ import { RoleMetadata } from '../entities/role-metadata.entity';
14
14
  @Injectable()
15
15
  export class RoleMetadataService extends CRUDService<RoleMetadata> {
16
16
  private readonly logger = new Logger(RoleMetadataService.name);
17
+ private readonly permissionAssignmentBatchSize = 500;
17
18
 
18
19
  constructor(
19
20
  @InjectEntityManager()
@@ -126,66 +127,47 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
126
127
  private async _addPermissionsToRole(roleName: string, permissionNames: string[]): Promise<RoleMetadata> {
127
128
  const role = await this.repo.findOne({
128
129
  where: { name: roleName },
129
- relations: {
130
- permissions: true
130
+ select: {
131
+ id: true,
132
+ name: true,
131
133
  }
132
134
  });
133
135
  if (!role) {
134
136
  throw new Error(`Role '${roleName}' not found.`);
135
137
  }
136
138
 
137
- // this.logger.log(`Found role ${roleName}`);
139
+ const joinInfo = this.getRolePermissionJoinInfo();
140
+ const targetPermissions = await this.loadTargetPermissions(permissionNames);
138
141
 
139
- // The new set of permissions which are to be added to this role.
140
- let newPermissions: PermissionMetadata[];
142
+ if (!permissionNames || permissionNames.length === 0) {
143
+ await this.entityManager
144
+ .createQueryBuilder()
145
+ .delete()
146
+ .from(joinInfo.tableName)
147
+ .where(`${joinInfo.roleIdColumn} = :roleId`, { roleId: role.id })
148
+ .execute();
141
149
 
142
- // Load all the specified permissions in the system.
143
- if (permissionNames && permissionNames.length != 0) {
144
- // this.logger.log(`Loading specified permissions.`);
145
-
146
- newPermissions = await this.permissionRepository.find({ where: { name: In(permissionNames) } });
147
- if (newPermissions.length !== permissionNames.length) {
148
- throw new Error(`One or more permissions not found.`);
149
- }
150
+ await this.insertRolePermissionMappings(role.id, targetPermissions.map((permission) => permission.id), joinInfo);
150
151
  }
151
152
  else {
152
- // this.logger.log(`Loading all permissions in system.`);
153
-
154
- // Load all permissions in the system.
155
- // TODO: Do we want to convert this to a paginated query to avoid having to load a very large permissions table into memory?
156
- newPermissions = await this.permissionRepository.find();
157
- if (newPermissions.length == 0) {
158
- throw new Error(`No permissions configured in the system. Did you forget to run the PermissionSeederService?`);
153
+ const existingRows = await this.entityManager
154
+ .createQueryBuilder()
155
+ .select(joinInfo.permissionIdColumn, 'permissionId')
156
+ .from(joinInfo.tableName, 'role_permission')
157
+ .where(`${joinInfo.roleIdColumn} = :roleId`, { roleId: role.id })
158
+ .getRawMany();
159
+
160
+ const existingPermissionIds = new Set(existingRows.map((row) => Number(row.permissionId)));
161
+ const missingPermissionIds = targetPermissions
162
+ .map((permission) => permission.id)
163
+ .filter((permissionId) => !existingPermissionIds.has(permissionId));
164
+
165
+ if (missingPermissionIds.length > 0) {
166
+ await this.insertRolePermissionMappings(role.id, missingPermissionIds, joinInfo);
159
167
  }
160
168
  }
161
169
 
162
- // this.logger.log(`Adding ${newPermissions.length} permissions to role ${roleName}.`);
163
-
164
- // if there are already permissions assigned.
165
- if (role.permissions && role.permissions.length > 0) {
166
- for (let i = 0; i < newPermissions.length; i++) {
167
- const newPermission = newPermissions[i];
168
- let newPermissionFound = true;
169
- for (let j = 0; j < role.permissions.length; j++) {
170
- const existingPermission = role.permissions[j];
171
- if (existingPermission.name === newPermission.name) {
172
- newPermissionFound = false;
173
- break;
174
- }
175
- }
176
-
177
- if (newPermissionFound) {
178
- role.permissions.push(newPermission);
179
- }
180
- }
181
-
182
- }
183
- // else we create a new permissions set.
184
- else {
185
- role.permissions = newPermissions;
186
- }
187
-
188
- return await this.repo.save(role);
170
+ return await this.findRoleByName(roleName);
189
171
  }
190
172
 
191
173
  async removePermissionsFromRole(roleName: string, permissionNames: string[]): Promise<RoleMetadata> {
@@ -220,5 +202,62 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
220
202
  return existingPermission;
221
203
  }
222
204
 
205
+ private async loadTargetPermissions(permissionNames: string[] | null): Promise<PermissionMetadata[]> {
206
+ if (permissionNames && permissionNames.length !== 0) {
207
+ const uniquePermissionNames = [...new Set(permissionNames)];
208
+ const permissions = await this.permissionRepository.find({ where: { name: In(uniquePermissionNames) } });
209
+ if (permissions.length !== uniquePermissionNames.length) {
210
+ throw new Error(`One or more permissions not found.`);
211
+ }
212
+ return permissions;
213
+ }
214
+
215
+ const permissions = await this.permissionRepository.find();
216
+ if (permissions.length === 0) {
217
+ throw new Error(`No permissions configured in the system. Did you forget to run the PermissionSeederService?`);
218
+ }
219
+ return permissions;
220
+ }
221
+
222
+ private getRolePermissionJoinInfo(): { tableName: string; roleIdColumn: string; permissionIdColumn: string } {
223
+ const relation = this.repo.metadata.relations.find((candidate) => candidate.propertyName === 'permissions');
224
+ if (!relation?.junctionEntityMetadata) {
225
+ throw new Error('Role-permission join table metadata not found.');
226
+ }
227
+
228
+ const roleIdColumn = relation.joinColumns[0]?.databaseName;
229
+ const permissionIdColumn = relation.inverseJoinColumns[0]?.databaseName;
230
+ if (!roleIdColumn || !permissionIdColumn) {
231
+ throw new Error('Role-permission join table column metadata not found.');
232
+ }
233
+
234
+ return {
235
+ tableName: relation.junctionEntityMetadata.tableName,
236
+ roleIdColumn,
237
+ permissionIdColumn,
238
+ };
239
+ }
240
+
241
+ private async insertRolePermissionMappings(
242
+ roleId: number,
243
+ permissionIds: number[],
244
+ joinInfo: { tableName: string; roleIdColumn: string; permissionIdColumn: string },
245
+ ): Promise<void> {
246
+ for (let index = 0; index < permissionIds.length; index += this.permissionAssignmentBatchSize) {
247
+ const batch = permissionIds.slice(index, index + this.permissionAssignmentBatchSize);
248
+ await this.entityManager
249
+ .createQueryBuilder()
250
+ .insert()
251
+ .into(joinInfo.tableName)
252
+ .values(
253
+ batch.map((permissionId) => ({
254
+ [joinInfo.roleIdColumn]: roleId,
255
+ [joinInfo.permissionIdColumn]: permissionId,
256
+ })),
257
+ )
258
+ .execute();
259
+ }
260
+ }
261
+
223
262
 
224
263
  }
@@ -21,6 +21,74 @@ import { MenuItemMetadata } from 'src/entities/menu-item-metadata.entity';
21
21
 
22
22
  @Injectable()
23
23
  export class ViewMetadataService extends CRUDService<ViewMetadata> {
24
+ private readonly comparableFields = [
25
+ 'name',
26
+ 'displayName',
27
+ 'type',
28
+ 'context',
29
+ 'layout',
30
+ 'module',
31
+ 'model',
32
+ ] as const;
33
+ private readonly relationCompareFields = new Set(['module', 'model']);
34
+ private readonly jsonCompareFields = new Set(['layout', 'context']);
35
+
36
+ private normalizeJsonFieldValue(value: any): any {
37
+ if (typeof value === 'string') {
38
+ const trimmedValue = value.trim();
39
+ if (
40
+ (trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
41
+ (trimmedValue.startsWith('[') && trimmedValue.endsWith(']'))
42
+ ) {
43
+ try {
44
+ return this.normalizeJsonFieldValue(JSON.parse(trimmedValue));
45
+ } catch {
46
+ return value;
47
+ }
48
+ }
49
+ return value;
50
+ }
51
+
52
+ if (Array.isArray(value)) {
53
+ return value.map((item) => this.normalizeJsonFieldValue(item));
54
+ }
55
+
56
+ if (value && typeof value === 'object') {
57
+ return Object.keys(value)
58
+ .sort()
59
+ .reduce((result, key) => {
60
+ result[key] = this.normalizeJsonFieldValue(value[key]);
61
+ return result;
62
+ }, {} as Record<string, any>);
63
+ }
64
+
65
+ return value;
66
+ }
67
+
68
+ private getCanonicalJsonFieldString(value: any): string {
69
+ return JSON.stringify(this.normalizeJsonFieldValue(value) ?? null);
70
+ }
71
+
72
+ private hasChanges(existingSolidView: ViewMetadata, updateSolidViewDto: UpdateViewMetadataDto): boolean {
73
+ return this.comparableFields.some((key) => {
74
+ const value = (updateSolidViewDto as any)[key];
75
+ if (typeof value === 'undefined') {
76
+ return false;
77
+ }
78
+
79
+ if (this.relationCompareFields.has(key)) {
80
+ const relationValue = value as any;
81
+ return (existingSolidView as any)[key]?.id !== relationValue?.id;
82
+ }
83
+
84
+ if (this.jsonCompareFields.has(key)) {
85
+ return this.getCanonicalJsonFieldString((existingSolidView as any)[key]) !== this.getCanonicalJsonFieldString(value);
86
+ }
87
+
88
+ return (existingSolidView as any)[key] !== value;
89
+ });
90
+ }
91
+
24
92
  constructor(
25
93
  readonly actionMetadataService: ActionMetadataService,
26
94
  readonly menuItemMetadataService: MenuItemMetadataService,
@@ -438,10 +506,20 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
438
506
 
439
507
  async upsert(updateSolidViewDto: UpdateViewMetadataDto) {
440
508
  // First check if module already exists using name
441
- const existingSolidView = await this.findOneByUserKey(updateSolidViewDto.name);
509
+ const existingSolidView = await this.findOneByUserKey(updateSolidViewDto.name, {
510
+ module: true,
511
+ model: true,
512
+ });
442
513
 
443
514
  // if found
444
515
  if (existingSolidView) {
516
+ const hasChanges = this.hasChanges(existingSolidView, updateSolidViewDto);
517
+
518
+ if (!hasChanges) {
519
+ this.logger.debug(`Skipping view upsert for ${updateSolidViewDto.name}; no changes detected.`);
520
+ return existingSolidView;
521
+ }
522
+
445
523
  const updatedSolidViewDto = { ...existingSolidView, ...updateSolidViewDto };
446
524
  return this.repo.save(updatedSolidViewDto);
447
525
  }
@@ -1,15 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(node -e:*)",
5
- "WebFetch(domain:docs.solidxai.com)",
6
- "WebFetch(domain:github.com)",
7
- "Read(//Users/oswald/projects/Solid_Starters/solidctl/**)",
8
- "Read(//Users/oswald/projects/Solid_Starters/**)",
9
- "Bash(find /Users/oswald/projects/Solid_Starters/solid-core-module -type d -name migration* -o -name *database*)"
10
- ],
11
- "additionalDirectories": [
12
- "/Users/oswald/projects/Solid_Starters/solidctl"
13
- ]
14
- }
15
- }
package/src/services/1.js DELETED
@@ -1,6 +0,0 @@
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