@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.
- package/.claude/settings.local.json +16 -0
- package/CHANGELOG.md +548 -0
- package/CLAUDE.md +26 -0
- package/dist/constants/draft-publish-fields.d.ts +2 -0
- package/dist/constants/draft-publish-fields.d.ts.map +1 -0
- package/dist/constants/draft-publish-fields.js +5 -0
- package/dist/constants/draft-publish-fields.js.map +1 -0
- package/dist/constants/media-file-types.d.ts +9 -0
- package/dist/constants/media-file-types.d.ts.map +1 -1
- package/dist/constants/media-file-types.js +27 -1
- package/dist/constants/media-file-types.js.map +1 -1
- package/dist/controllers/action-metadata.controller.d.ts +1 -1
- package/dist/controllers/action-metadata.controller.js +1 -1
- package/dist/controllers/agent-event.controller.d.ts +1 -1
- package/dist/controllers/agent-event.controller.js +1 -1
- package/dist/controllers/agent-session.controller.d.ts +1 -1
- package/dist/controllers/agent-session.controller.js +1 -1
- package/dist/controllers/dashboard-user-layout.controller.d.ts +1 -1
- package/dist/controllers/dashboard-user-layout.controller.js +1 -1
- package/dist/controllers/email-template.controller.d.ts +1 -1
- package/dist/controllers/email-template.controller.js +1 -1
- package/dist/controllers/export-template.controller.d.ts +1 -1
- package/dist/controllers/export-template.controller.d.ts.map +1 -1
- package/dist/controllers/export-template.controller.js +6 -4
- package/dist/controllers/export-template.controller.js.map +1 -1
- package/dist/controllers/export-transaction.controller.d.ts +1 -1
- package/dist/controllers/export-transaction.controller.js +1 -1
- package/dist/controllers/import-transaction-error-log.controller.d.ts +1 -1
- package/dist/controllers/import-transaction-error-log.controller.js +1 -1
- package/dist/controllers/import-transaction.controller.d.ts +1 -1
- package/dist/controllers/import-transaction.controller.d.ts.map +1 -1
- package/dist/controllers/import-transaction.controller.js +7 -7
- package/dist/controllers/import-transaction.controller.js.map +1 -1
- package/dist/controllers/list-of-values.controller.d.ts +1 -1
- package/dist/controllers/list-of-values.controller.js +1 -1
- package/dist/controllers/locale.controller.d.ts +1 -1
- package/dist/controllers/locale.controller.js +1 -1
- package/dist/controllers/mcp-audit-log.controller.d.ts +1 -1
- package/dist/controllers/mcp-audit-log.controller.js +1 -1
- package/dist/controllers/media.controller.d.ts +1 -1
- package/dist/controllers/media.controller.d.ts.map +1 -1
- package/dist/controllers/media.controller.js +12 -7
- package/dist/controllers/media.controller.js.map +1 -1
- package/dist/controllers/menu-item-metadata.controller.d.ts +1 -1
- package/dist/controllers/menu-item-metadata.controller.js +1 -1
- package/dist/controllers/model-sequence.controller.d.ts +1 -1
- package/dist/controllers/model-sequence.controller.js +1 -1
- package/dist/controllers/module-package.controller.d.ts.map +1 -1
- package/dist/controllers/module-package.controller.js +2 -3
- package/dist/controllers/module-package.controller.js.map +1 -1
- package/dist/controllers/mq-message-queue.controller.d.ts +1 -1
- package/dist/controllers/mq-message-queue.controller.js +1 -1
- package/dist/controllers/mq-message.controller.d.ts +1 -1
- package/dist/controllers/mq-message.controller.js +1 -1
- package/dist/controllers/permission-metadata.controller.d.ts +1 -1
- package/dist/controllers/permission-metadata.controller.js +1 -1
- package/dist/controllers/role-metadata.controller.d.ts +1 -1
- package/dist/controllers/role-metadata.controller.js +1 -1
- package/dist/controllers/saved-filters.controller.d.ts +1 -1
- package/dist/controllers/saved-filters.controller.js +1 -1
- package/dist/controllers/scheduled-job.controller.d.ts +1 -1
- package/dist/controllers/scheduled-job.controller.js +1 -1
- package/dist/controllers/security-rule.controller.d.ts +1 -1
- package/dist/controllers/security-rule.controller.js +1 -1
- package/dist/controllers/sms-template.controller.d.ts +1 -1
- package/dist/controllers/sms-template.controller.js +1 -1
- package/dist/controllers/user-activity-history.controller.d.ts +1 -1
- package/dist/controllers/user-activity-history.controller.js +1 -1
- package/dist/controllers/user-view-metadata.controller.d.ts +1 -1
- package/dist/controllers/user-view-metadata.controller.js +1 -1
- package/dist/controllers/user.controller.d.ts +2 -2
- package/dist/controllers/user.controller.js +2 -2
- package/dist/controllers/view-metadata.controller.d.ts +2 -2
- package/dist/controllers/view-metadata.controller.js +1 -1
- package/dist/controllers/workflow-definition.controller.d.ts +1 -1
- package/dist/controllers/workflow-definition.controller.js +1 -1
- package/dist/controllers/workflow-execution-artifact.controller.d.ts +1 -1
- package/dist/controllers/workflow-execution-artifact.controller.js +1 -1
- package/dist/controllers/workflow-execution-log.controller.d.ts +1 -1
- package/dist/controllers/workflow-execution-log.controller.js +1 -1
- package/dist/controllers/workflow-execution.controller.d.ts +1 -1
- package/dist/controllers/workflow-execution.controller.js +1 -1
- package/dist/controllers/workflow-secret.controller.d.ts +1 -1
- package/dist/controllers/workflow-secret.controller.js +1 -1
- package/dist/controllers/workflow-step-execution.controller.d.ts +1 -1
- package/dist/controllers/workflow-step-execution.controller.js +1 -1
- package/dist/controllers/workflow-trigger-execution.controller.d.ts +1 -1
- package/dist/controllers/workflow-trigger-execution.controller.js +1 -1
- package/dist/dtos/basic-filters.dto.d.ts +0 -1
- package/dist/dtos/basic-filters.dto.d.ts.map +1 -1
- package/dist/dtos/basic-filters.dto.js +1 -6
- package/dist/dtos/basic-filters.dto.js.map +1 -1
- package/dist/entities/common.entity.d.ts +4 -0
- package/dist/entities/common.entity.d.ts.map +1 -1
- package/dist/entities/common.entity.js +25 -1
- package/dist/entities/common.entity.js.map +1 -1
- package/dist/entities/user.entity.js +2 -2
- package/dist/entities/user.entity.js.map +1 -1
- package/dist/factories/mail.factory.d.ts.map +1 -1
- package/dist/factories/mail.factory.js +4 -0
- package/dist/factories/mail.factory.js.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts +1 -0
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +18 -11
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
- package/dist/helpers/file-download.helper.d.ts +9 -0
- package/dist/helpers/file-download.helper.d.ts.map +1 -0
- package/dist/helpers/file-download.helper.js +14 -0
- package/dist/helpers/file-download.helper.js.map +1 -0
- package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
- package/dist/helpers/model-metadata-helper.service.js +30 -1
- package/dist/helpers/model-metadata-helper.service.js.map +1 -1
- package/dist/helpers/solid-registry.js +1 -1
- package/dist/helpers/solid-registry.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/seeders/module-test-data.service.d.ts +3 -1
- package/dist/seeders/module-test-data.service.d.ts.map +1 -1
- package/dist/seeders/module-test-data.service.js +8 -2
- package/dist/seeders/module-test-data.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +2 -0
- package/dist/services/authentication.service.d.ts +4 -1
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +52 -4
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/chatter-message.service.d.ts +1 -0
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +8 -0
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/crud-helper.service.d.ts +5 -2
- package/dist/services/crud-helper.service.d.ts.map +1 -1
- package/dist/services/crud-helper.service.js +30 -32
- package/dist/services/crud-helper.service.js.map +1 -1
- package/dist/services/crud.service.d.ts +11 -1
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +79 -66
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/datasource-introspection.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection.service.js.map +1 -1
- package/dist/services/draft-publish-helper.service.d.ts +52 -0
- package/dist/services/draft-publish-helper.service.d.ts.map +1 -0
- package/dist/services/draft-publish-helper.service.js +368 -0
- package/dist/services/draft-publish-helper.service.js.map +1 -0
- package/dist/services/internationalisation-helper.service.d.ts +19 -0
- package/dist/services/internationalisation-helper.service.d.ts.map +1 -0
- package/dist/services/internationalisation-helper.service.js +107 -0
- package/dist/services/internationalisation-helper.service.js.map +1 -0
- package/dist/services/locale.service.d.ts +1 -1
- package/dist/services/media.service.d.ts +2 -1
- package/dist/services/media.service.d.ts.map +1 -1
- package/dist/services/media.service.js +28 -2
- package/dist/services/media.service.js.map +1 -1
- package/dist/services/metadata-validation.service.d.ts +7 -0
- package/dist/services/metadata-validation.service.d.ts.map +1 -0
- package/dist/services/metadata-validation.service.js +42 -0
- package/dist/services/metadata-validation.service.js.map +1 -0
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +2 -1
- package/dist/services/model-metadata.service.js.map +1 -1
- package/dist/services/object.utils.d.ts +2 -0
- package/dist/services/object.utils.d.ts.map +1 -0
- package/dist/services/object.utils.js +11 -0
- package/dist/services/object.utils.js.map +1 -0
- package/dist/services/settings/default-settings-provider.service.d.ts +6 -6
- package/dist/services/settings/default-settings-provider.service.js +3 -3
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/user.service.d.ts +2 -2
- package/dist/services/user.service.d.ts.map +1 -1
- package/dist/services/user.service.js +9 -3
- package/dist/services/user.service.js.map +1 -1
- package/dist/services/view-metadata.service.d.ts +1 -2
- package/dist/services/view-metadata.service.d.ts.map +1 -1
- package/dist/services/view-metadata.service.js +3 -55
- package/dist/services/view-metadata.service.js.map +1 -1
- package/dist/services/workflow-definition.service.d.ts +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +10 -1
- package/dist/solid-core.module.js.map +1 -1
- package/package.json +1 -1
- package/src/constants/draft-publish-fields.ts +5 -0
- package/src/constants/media-file-types.ts +50 -0
- package/src/controllers/export-template.controller.ts +5 -3
- package/src/controllers/import-transaction.controller.ts +6 -6
- package/src/controllers/media.controller.ts +17 -6
- package/src/controllers/module-package.controller.ts +2 -3
- package/src/dtos/basic-filters.dto.ts +0 -4
- package/src/entities/common.entity.ts +20 -0
- package/src/entities/user.entity.ts +2 -2
- package/src/factories/mail.factory.ts +7 -1
- package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +26 -12
- package/src/helpers/file-download.helper.ts +31 -0
- package/src/helpers/model-metadata-helper.service.ts +32 -1
- package/src/helpers/solid-registry.ts +1 -1
- package/src/index.ts +2 -0
- package/src/seeders/module-test-data.service.ts +6 -0
- package/src/seeders/seed-data/solid-core-metadata.json +2 -0
- package/src/services/1.js +6 -0
- package/src/services/authentication.service.ts +76 -5
- package/src/services/chatter-message.service.ts +16 -0
- package/src/services/crud-helper.service.ts +16 -34
- package/src/services/crud.service.ts +98 -103
- package/src/services/datasource-introspection.service.ts +5 -0
- package/src/services/draft-publish-helper.service.ts +529 -0
- package/src/services/internationalisation-helper.service.ts +148 -0
- package/src/services/media.service.ts +48 -3
- package/src/services/metadata-validation.service.ts +39 -0
- package/src/services/model-metadata.service.ts +4 -1
- package/src/services/object.utils.ts +10 -0
- package/src/services/settings/default-settings-provider.service.ts +3 -3
- package/src/services/user.service.ts +18 -3
- package/src/services/view-metadata.service.ts +5 -145
- package/src/solid-core.module.ts +16 -3
- package/dist-tests/api/authenticate.spec.js +0 -119
- package/dist-tests/api/authenticate.spec.js.map +0 -1
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
- package/dist-tests/api/ping.spec.js +0 -21
- package/dist-tests/api/ping.spec.js.map +0 -1
- package/dist-tests/helpers/auth.js +0 -41
- package/dist-tests/helpers/auth.js.map +0 -1
- package/dist-tests/helpers/env.js +0 -11
- package/dist-tests/helpers/env.js.map +0 -1
- package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
- package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
- package/docs/agent-hub-grooming.md +0 -301
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
- package/docs/datasource-introspection-ddl-analysis.md +0 -326
- package/docs/datasource-introspection-implementation-plan.md +0 -306
- package/docs/grouping-enhancements.md +0 -89
- package/docs/java-spring/README.md +0 -3
- package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
- package/docs/module-package-import-handoff.md +0 -691
- package/docs/seed-changes.md +0 -65
- package/docs/test-data-workflow.md +0 -200
- package/docs/type-declaration-import-issue.md +0 -24
- package/docs/workflow/business-automation-example-notes.md +0 -309
- package/docs/workflow/control-flow-node-addition-sop.md +0 -324
- package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
- package/docs/workflow/foreach-example-notes.md +0 -187
- package/docs/workflow/hello-world-example-notes.md +0 -271
- package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
- package/docs/workflow/loop-until-example-notes.md +0 -198
- package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
- package/docs/workflow/samples.md +0 -25
- package/docs/workflow/what-is-kestra.md +0 -79
- package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
- package/docs/workflow/workflow-module-brd.md +0 -185
- package/docs/workflow/workflow-module-domain-model.md +0 -252
- package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
- package/docs/workflow/workflow-module-technical-spec.md +0 -293
- package/docs/workflow/workflow-node-type-contracts.md +0 -635
- package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
- package/docs/workflow/workflow-solidx-model-field-spec.md +0 -284
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { BadRequestException, forwardRef, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef } from "@nestjs/core";
|
|
3
3
|
import { InjectEntityManager } from '@nestjs/typeorm';
|
|
4
|
-
import { EntityManager, In } from 'typeorm';
|
|
4
|
+
import { EntityManager, In, Not } from 'typeorm';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { Readable } from 'stream';
|
|
7
7
|
|
|
8
8
|
import { ConfigService } from '@nestjs/config';
|
|
9
9
|
import { CRUDService } from 'src/services/crud.service';
|
|
10
10
|
import { DiskFileService, S3FileService } from 'src/services/file';
|
|
11
|
+
import { classify } from 'src/helpers/string.helper';
|
|
11
12
|
import { MediaDownloadUrlService } from 'src/services/media-download-url.service';
|
|
12
13
|
|
|
13
14
|
|
|
@@ -176,9 +177,13 @@ export class MediaService extends CRUDService<Media> {
|
|
|
176
177
|
async delete(id: number, solidRequestContext: any = {}) {
|
|
177
178
|
const media = await this.repo.findOne({
|
|
178
179
|
where: { id },
|
|
179
|
-
relations: ['mediaStorageProviderMetadata'],
|
|
180
|
+
relations: ['mediaStorageProviderMetadata', 'modelMetadata'],
|
|
180
181
|
});
|
|
181
182
|
|
|
183
|
+
if (media) {
|
|
184
|
+
await this.assertMediaDeletable([media]);
|
|
185
|
+
}
|
|
186
|
+
|
|
182
187
|
const result = await super.delete(id, solidRequestContext);
|
|
183
188
|
|
|
184
189
|
if (media) {
|
|
@@ -193,9 +198,11 @@ export class MediaService extends CRUDService<Media> {
|
|
|
193
198
|
where: {
|
|
194
199
|
id: In(ids),
|
|
195
200
|
},
|
|
196
|
-
relations: ['mediaStorageProviderMetadata'],
|
|
201
|
+
relations: ['mediaStorageProviderMetadata', 'modelMetadata'],
|
|
197
202
|
});
|
|
198
203
|
|
|
204
|
+
await this.assertMediaDeletable(mediaRecords);
|
|
205
|
+
|
|
199
206
|
const result = await super.deleteMany(ids, solidRequestContext);
|
|
200
207
|
|
|
201
208
|
for (const media of mediaRecords) {
|
|
@@ -205,6 +212,33 @@ export class MediaService extends CRUDService<Media> {
|
|
|
205
212
|
return result;
|
|
206
213
|
}
|
|
207
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Media belonging to a published draft/publish-enabled record must survive edits made to
|
|
217
|
+
* later drafts (see DraftPublishHelperService.cloneMediaForVersion) - deleting it directly
|
|
218
|
+
* would strip the published version's media out from under it. Mirrors the equivalent
|
|
219
|
+
* entity-level guard in DraftPublishHelperService.assertDraftPublishDeleteAllowed.
|
|
220
|
+
*/
|
|
221
|
+
private async assertMediaDeletable(mediaRecords: Media[]): Promise<void> {
|
|
222
|
+
const blockedIds: number[] = [];
|
|
223
|
+
|
|
224
|
+
for (const media of mediaRecords) {
|
|
225
|
+
if (!media.modelMetadata?.draftPublishWorkflow) continue;
|
|
226
|
+
|
|
227
|
+
const entityRepository = this.entityManager.getRepository(classify(media.modelMetadata.singularName));
|
|
228
|
+
const owningEntity = await entityRepository.findOne({ where: { id: media.entityId } as any });
|
|
229
|
+
|
|
230
|
+
if (owningEntity && (owningEntity as any).isPublished === true) {
|
|
231
|
+
blockedIds.push(media.id);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (blockedIds.length > 0) {
|
|
236
|
+
throw new BadRequestException(
|
|
237
|
+
'Media belonging to a published record cannot be deleted. Edit the record to create a draft first'
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
208
242
|
private async decorateMediaRecords(medias: Media[]): Promise<void> {
|
|
209
243
|
for (const media of medias) {
|
|
210
244
|
await this.decorateMediaRecord(media);
|
|
@@ -244,6 +278,17 @@ export class MediaService extends CRUDService<Media> {
|
|
|
244
278
|
}
|
|
245
279
|
|
|
246
280
|
try {
|
|
281
|
+
// Draft/publish versioning may clone a Media row that reuses the same
|
|
282
|
+
// relativeUri (see DraftPublishHelperService.cloneMediaForVersion) instead of
|
|
283
|
+
// duplicating the underlying file. Skip the physical delete if another row
|
|
284
|
+
// still references it.
|
|
285
|
+
const stillReferenced = await this.repo.exists({
|
|
286
|
+
where: { relativeUri: media.relativeUri, id: Not(media.id) },
|
|
287
|
+
});
|
|
288
|
+
if (stillReferenced) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
247
292
|
const mediaStorageProvider = await this.resolveMediaStorageProvider(media);
|
|
248
293
|
if (!mediaStorageProvider) {
|
|
249
294
|
return;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BadRequestException, Injectable } from "@nestjs/common";
|
|
2
|
+
import { isNotEmpty, matches } from "class-validator";
|
|
3
|
+
import { ModelMetadataService } from "./model-metadata.service";
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class MetadataValidationService {
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly modelMetadataService: ModelMetadataService,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
async validateCreateDto(
|
|
12
|
+
modelName: string,
|
|
13
|
+
dto: object,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const model = await this.modelMetadataService.findOneBySingularName(
|
|
16
|
+
modelName,
|
|
17
|
+
{ fields: true },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
for (const field of model.fields ?? []) {
|
|
21
|
+
const value = (dto as Record<string, unknown>)[field.name];
|
|
22
|
+
|
|
23
|
+
if (!isNotEmpty(value) || typeof value !== "string") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!isNotEmpty(field.regexPattern)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!matches(value, new RegExp(field.regexPattern))) {
|
|
32
|
+
throw new BadRequestException(
|
|
33
|
+
field.regexPatternNotMatchingErrorMsg ||
|
|
34
|
+
`Validation errors in ${field.name} is invalid i.e Field regex pattern is invalid`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1729,7 +1729,10 @@ export class ModelMetadataService {
|
|
|
1729
1729
|
// --------------------
|
|
1730
1730
|
// Find record index in page
|
|
1731
1731
|
// --------------------
|
|
1732
|
-
const index = records.findIndex(r =>
|
|
1732
|
+
const index = records.findIndex(r =>
|
|
1733
|
+
String(r.id) === String(recordId) ||
|
|
1734
|
+
(r.initialEntityVersionId && String(r.initialEntityVersionId) === String(recordId))
|
|
1735
|
+
);
|
|
1733
1736
|
|
|
1734
1737
|
if (index === -1) {
|
|
1735
1738
|
throw new BadRequestException(`Record not found in current page`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Some filter/sort DTOs arrive with bracket-wrapped keys (e.g. "[isPublished]") from
|
|
2
|
+
// query-string parsing. Both CrudHelperService and DraftPublishHelperService need to
|
|
3
|
+
// strip that wrapping before reading keys, so it lives here as a shared utility.
|
|
4
|
+
export function normalizeObjectKeys(obj: any): any {
|
|
5
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
6
|
+
const newKey = key.replace(/^\[(.*)\]$/, '$1');
|
|
7
|
+
acc[newKey] = obj[key];
|
|
8
|
+
return acc;
|
|
9
|
+
}, {});
|
|
10
|
+
}
|
|
@@ -125,10 +125,10 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
moduleName: "solid-core",
|
|
128
|
-
key: "
|
|
128
|
+
key: "recordClickAction",
|
|
129
129
|
value: "edit",
|
|
130
130
|
level: SettingLevel.SystemAdminEditable,
|
|
131
|
-
label: "
|
|
131
|
+
label: "Record Click Action",
|
|
132
132
|
group: "app-settings",
|
|
133
133
|
sortOrder: 45,
|
|
134
134
|
controlType: "selectionStatic",
|
|
@@ -136,7 +136,7 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
136
136
|
{ label: "Edit", value: "edit" },
|
|
137
137
|
{ label: "View", value: "view" },
|
|
138
138
|
],
|
|
139
|
-
helpText: "Decides whether clicking a
|
|
139
|
+
helpText: "Decides whether clicking a record in List, Card, Kanban, or Tree View opens it in edit mode or view mode.",
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
moduleName: "solid-core",
|
|
@@ -156,10 +156,25 @@ export class UserService extends CRUDService<User> {
|
|
|
156
156
|
if (!user) {
|
|
157
157
|
throw new Error(ERROR_MESSAGES.USER_NOT_FOUND);
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
|
|
160
|
+
const { roles, ...userUpdateDto } = updateDto || {};
|
|
161
|
+
const savedUser = await this.update(
|
|
162
|
+
id,
|
|
163
|
+
userUpdateDto,
|
|
164
|
+
files,
|
|
165
|
+
true,
|
|
166
|
+
solidRequestContext,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (roles != null) {
|
|
170
|
+
await this.addRolesToUser(savedUser.username, roles);
|
|
171
|
+
return this.repo.findOne({
|
|
172
|
+
where: { id: savedUser.id },
|
|
173
|
+
relations: { roles: true },
|
|
174
|
+
});
|
|
161
175
|
}
|
|
162
|
-
|
|
176
|
+
|
|
177
|
+
return savedUser;
|
|
163
178
|
}
|
|
164
179
|
|
|
165
180
|
async addRoleToUser(username: string, roleName: string): Promise<User> {
|
|
@@ -4,8 +4,6 @@ import { InjectEntityManager } from '@nestjs/typeorm';
|
|
|
4
4
|
import { CRUDService } from 'src/services/crud.service';
|
|
5
5
|
import { EntityManager } from 'typeorm';
|
|
6
6
|
|
|
7
|
-
import { classify } from '../helpers/string.helper';
|
|
8
|
-
import { Locale } from 'src/entities/locale.entity';
|
|
9
7
|
import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
|
|
10
8
|
import { SolidRegistry } from 'src/helpers/solid-registry';
|
|
11
9
|
import { ModelMetadataRepository } from 'src/repository/model-metadata.repository';
|
|
@@ -197,64 +195,6 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
|
197
195
|
return this.getDefaultWorkflowFieldData(workflowField, workflowFieldName, fieldsMap);
|
|
198
196
|
}
|
|
199
197
|
|
|
200
|
-
//for locales
|
|
201
|
-
private async getEntityRecordsInAllLocales(
|
|
202
|
-
modelName: string,
|
|
203
|
-
id: string,
|
|
204
|
-
defaultEntityLocaleIdFromQuery?: string
|
|
205
|
-
): Promise<{ records: any[], defaultEntityLocaleId: string | null }> {
|
|
206
|
-
const solidRegistry = await this.moduleRef.get(SolidRegistry, { strict: false });
|
|
207
|
-
// const currentEntityTarget = solidRegistry.getEntityTarget(this.entityManager, classify(modelName));
|
|
208
|
-
const currentEntityRepository = this.entityManager.getRepository(classify(modelName));
|
|
209
|
-
|
|
210
|
-
// Case 1: Creating a new record with no defaultEntityLocaleId to clone
|
|
211
|
-
if (id === 'new' && !defaultEntityLocaleIdFromQuery) {
|
|
212
|
-
this.logger.debug(`Creating new record without cloning from any defaultEntityLocaleId.`);
|
|
213
|
-
return { records: [], defaultEntityLocaleId: null };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Case 2: Creating a new record and cloning from an existing defaultEntityLocaleId
|
|
217
|
-
if (id === 'new' && defaultEntityLocaleIdFromQuery) {
|
|
218
|
-
this.logger.debug(`Creating new record by cloning translations from defaultEntityLocaleId: ${defaultEntityLocaleIdFromQuery}`);
|
|
219
|
-
|
|
220
|
-
const records = await currentEntityRepository.find({
|
|
221
|
-
where: [
|
|
222
|
-
{ defaultEntityLocaleId: defaultEntityLocaleIdFromQuery },
|
|
223
|
-
{ id: defaultEntityLocaleIdFromQuery }
|
|
224
|
-
]
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
this.logger.debug(`Found ${records.length} cloned records for new entity.`);
|
|
228
|
-
return { records, defaultEntityLocaleId: defaultEntityLocaleIdFromQuery };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Case 3: Editing an existing entity
|
|
232
|
-
const entityRecord = await currentEntityRepository.findOne({ where: { id } });
|
|
233
|
-
|
|
234
|
-
if (!entityRecord) {
|
|
235
|
-
this.logger.warn(`No entity found for id ${id}`);
|
|
236
|
-
return { records: [], defaultEntityLocaleId: null };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const defaultEntityLocaleId = entityRecord.defaultEntityLocaleId || entityRecord.id;
|
|
240
|
-
if (entityRecord.defaultEntityLocaleId) {
|
|
241
|
-
this.logger.debug(`Editing translated locale record. Translation root id: ${defaultEntityLocaleId}`);
|
|
242
|
-
} else {
|
|
243
|
-
this.logger.debug(`Editing translation root record with id ${defaultEntityLocaleId}`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const records = await currentEntityRepository.find({
|
|
247
|
-
where: [
|
|
248
|
-
{ defaultEntityLocaleId: defaultEntityLocaleId },
|
|
249
|
-
{ id: defaultEntityLocaleId }
|
|
250
|
-
]
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
this.logger.debug(`Found ${records.length} records in all locales for existing entity.`);
|
|
254
|
-
|
|
255
|
-
return { records, defaultEntityLocaleId };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
198
|
// START: Custom Service Methods
|
|
259
199
|
async getLayout(query, activeUser) {
|
|
260
200
|
let { modelName, moduleName, viewType, id, populate, menuItemId, menuItemName, actionId, actionName } = query;
|
|
@@ -432,92 +372,12 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
|
432
372
|
* ]
|
|
433
373
|
*/
|
|
434
374
|
|
|
435
|
-
|
|
436
|
-
// if (entity.model.internationalisation) {
|
|
437
|
-
// const allLocales = await this.entityManager.getRepository(Locale).find({});
|
|
438
|
-
|
|
439
|
-
// if (id === 'new') {
|
|
440
|
-
// allLocales.forEach(locale => {
|
|
441
|
-
// applicableLocales.push({
|
|
442
|
-
// locale: locale.locale,
|
|
443
|
-
// displayName: locale.displayName,
|
|
444
|
-
// isDefault: locale.isDefault ? 'yes' : 'no',
|
|
445
|
-
// defaultEntityLocaleId: null,
|
|
446
|
-
// entityId: null
|
|
447
|
-
// });
|
|
448
|
-
// });
|
|
449
|
-
// }
|
|
450
|
-
// else {
|
|
451
|
-
// const defaultLocale = allLocales.find(locale => locale.isDefault);
|
|
452
|
-
// this.logger.debug(`Default locale is: ${defaultLocale.locale}`);
|
|
453
|
-
|
|
454
|
-
// // Get hold of the repository for the current model
|
|
455
|
-
// const solidRegistry = await this.moduleRef.get(SolidRegistry, { strict: false });
|
|
456
|
-
// const currentEntityTarget = solidRegistry.getEntityTarget(this.entityManager, classify(modelName));
|
|
457
|
-
// const currentEntityRepository = this.entityManager.getRepository(currentEntityTarget);
|
|
458
|
-
|
|
459
|
-
// // We are in edit mode, the id that is being edited could be a record tagged with the default locale or it could be tagged with a non-default locale.
|
|
460
|
-
// const entityRecord = await currentEntityRepository.findOne({
|
|
461
|
-
// where: {
|
|
462
|
-
// id: id,
|
|
463
|
-
// }
|
|
464
|
-
// });
|
|
465
|
-
// if(entityRecord){
|
|
466
|
-
// // Resolve the default entity locale id....
|
|
467
|
-
// let defaultEntityLocaleId = null;
|
|
468
|
-
// if (entityRecord.localeName === defaultLocale.locale) {
|
|
469
|
-
// defaultEntityLocaleId = entityRecord.id;
|
|
470
|
-
// this.logger.debug(`You are editing a record tagged with the default locale: ${entityRecord.localeName}.`);
|
|
471
|
-
// }
|
|
472
|
-
// else {
|
|
473
|
-
// defaultEntityLocaleId = entityRecord.defaultEntityLocaleId;
|
|
474
|
-
// this.logger.debug(`You are editing a record tagged with the non-default locale: ${entityRecord.localeName}. `);
|
|
475
|
-
// }
|
|
476
|
-
// this.logger.debug(`Identified default Entity Locale Id: ${defaultEntityLocaleId}`);
|
|
477
|
-
|
|
478
|
-
// // Now we query for all records in the same model matching the defaultEntityLocaleId
|
|
479
|
-
// // Get all records mathcing the defaultEntityLocaleId or where the id is same as the defaultEntityLocaleId
|
|
480
|
-
// const entityRecordsInAllLocales = await currentEntityRepository.find({
|
|
481
|
-
// where: [
|
|
482
|
-
// { defaultEntityLocaleId: defaultEntityLocaleId },
|
|
483
|
-
// { id: defaultEntityLocaleId }
|
|
484
|
-
// ],
|
|
485
|
-
// });
|
|
486
|
-
// this.logger.debug(`Found ${entityRecordsInAllLocales.length} records in all locales for the defaultEntityLocaleId: ${defaultEntityLocaleId}`);
|
|
487
|
-
|
|
488
|
-
// // Loop over all locales and populate the applicableLocales array
|
|
489
|
-
// for (const locale of allLocales) {
|
|
490
|
-
// // Find the record in the entityRecordsInAllLocales that matches the current locale
|
|
491
|
-
// const matchingRecord = entityRecordsInAllLocales.find(record => record.localeName === locale.locale);
|
|
492
|
-
|
|
493
|
-
// applicableLocales.push({
|
|
494
|
-
// locale: locale.locale,
|
|
495
|
-
// displayName: locale.displayName,
|
|
496
|
-
// isDefault: locale.isDefault ? 'yes' : 'no',
|
|
497
|
-
// defaultEntityLocaleId: defaultEntityLocaleId,
|
|
498
|
-
// entityId: (matchingRecord ? matchingRecord.id : null)
|
|
499
|
-
// });
|
|
500
|
-
// }
|
|
501
|
-
// }else{
|
|
502
|
-
// this.logger.warn(`No record found for id: ${id} in model: ${modelName}. Cannot determine applicable locales.`);
|
|
503
|
-
// }
|
|
504
|
-
// }
|
|
505
|
-
// }
|
|
375
|
+
let applicableLocales: any[] = [];
|
|
506
376
|
if (entity.model.internationalisation) {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
for (const locale of allLocales) {
|
|
512
|
-
const matchingRecord = entityRecordsInAllLocales.find(record => record.localeName === locale.locale);
|
|
513
|
-
applicableLocales.push({
|
|
514
|
-
locale: locale.locale,
|
|
515
|
-
displayName: locale.displayName,
|
|
516
|
-
isDefault: locale.isDefault ? 'yes' : 'no',
|
|
517
|
-
defaultEntityLocaleId: defaultEntityLocaleId,
|
|
518
|
-
entityId: matchingRecord ? matchingRecord.id : null
|
|
519
|
-
});
|
|
520
|
-
}
|
|
377
|
+
const draftPublishEnabled = entity.model.draftPublishWorkflow === true;
|
|
378
|
+
applicableLocales = await this.internationalisationHelperService.buildApplicableLocales(
|
|
379
|
+
this.entityManager, modelName, id, query?.defaultEntityLocaleId, draftPublishEnabled,
|
|
380
|
+
);
|
|
521
381
|
}
|
|
522
382
|
|
|
523
383
|
const r = {
|
package/src/solid-core.module.ts
CHANGED
|
@@ -38,6 +38,8 @@ import { MqDashboardQueueNameVariableOptionsProvider } from "./services/selectio
|
|
|
38
38
|
import { PseudoForeignKeySelectionProvider } from "./services/selection-providers/pseudo-foreign-key-selection-provider.service";
|
|
39
39
|
import { ModuleMetadataSeederService } from "./seeders/module-metadata-seeder.service";
|
|
40
40
|
import { ModuleTestDataService } from "./seeders/module-test-data.service";
|
|
41
|
+
import { DraftPublishHelperService } from "./services/draft-publish-helper.service";
|
|
42
|
+
import { InternationalisationHelperService } from "./services/internationalisation-helper.service";
|
|
41
43
|
import { CrudHelperService } from "./services/crud-helper.service";
|
|
42
44
|
import { DatasourceManagementService } from "./services/datasource-management.service";
|
|
43
45
|
import { DatasourceIntrospectionService } from "./services/datasource-introspection.service";
|
|
@@ -56,10 +58,10 @@ import { ModulePackageService } from "./services/module-package.service";
|
|
|
56
58
|
import { SolidIntrospectService } from "./services/solid-introspect.service";
|
|
57
59
|
// import { ListOfComputedFieldProvider } from './providers/list-of-computed-field-provider.service';
|
|
58
60
|
import { ServeStaticModule } from "@nestjs/serve-static";
|
|
59
|
-
import {
|
|
61
|
+
import { basename, join } from "path";
|
|
60
62
|
import { RefreshModelCommand } from "./commands/refresh-model.command";
|
|
61
63
|
import { MediaController } from "./controllers/media.controller";
|
|
62
|
-
import { INLINE_SAFE_EXTENSIONS } from "./constants/media-file-types";
|
|
64
|
+
import { getLowercaseFileExtension, INLINE_SAFE_EXTENSIONS } from "./constants/media-file-types";
|
|
63
65
|
|
|
64
66
|
import { RefreshModuleCommand } from "./commands/refresh-module.command";
|
|
65
67
|
import { ModelMetadataSubscriber } from "./subscribers/model-metadata.subscriber";
|
|
@@ -167,6 +169,7 @@ import { GoogleOauthStrategy } from "./passport-strategies/google-oauth.strategy
|
|
|
167
169
|
import { ApiKeyService } from "./services/api-key.service";
|
|
168
170
|
import { ActiveSessionStorageService } from "./services/active-session-storage.service";
|
|
169
171
|
import { AuthenticationService } from "./services/authentication.service";
|
|
172
|
+
import { MetadataValidationService } from "./services/metadata-validation.service";
|
|
170
173
|
import { BcryptService } from "./services/bcrypt.service";
|
|
171
174
|
import { UuidExternalIdEntityComputedFieldProvider } from "./services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service";
|
|
172
175
|
import { UuidExternalIdComputedFieldProvider } from "./services/computed-fields/uuid-external-id-computed-field-provider.service";
|
|
@@ -535,7 +538,11 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
|
|
|
535
538
|
// else (including svg, html, and any other uploaded file) is forced to download
|
|
536
539
|
// rather than be displayed/executed by the browser, regardless of what mimetype or
|
|
537
540
|
// extension it was uploaded with.
|
|
538
|
-
|
|
541
|
+
// basename first: getLowercaseFileExtension takes a file name, not a path, so a
|
|
542
|
+
// directory containing a dot must not be mistaken for the extension. An
|
|
543
|
+
// extensionless file yields undefined, which is not inline-safe - so it correctly
|
|
544
|
+
// falls through to the forced-download branch.
|
|
545
|
+
if (!INLINE_SAFE_EXTENSIONS.has(getLowercaseFileExtension(basename(path)) ?? '')) {
|
|
539
546
|
res.setHeader("Content-Type", "application/octet-stream");
|
|
540
547
|
res.setHeader("Content-Disposition", "attachment");
|
|
541
548
|
}
|
|
@@ -644,6 +651,7 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
|
|
|
644
651
|
ModuleMetadataExplorerService,
|
|
645
652
|
ModuleMetadataHelperService,
|
|
646
653
|
ModulePackageService,
|
|
654
|
+
MetadataValidationService,
|
|
647
655
|
ModelMetadataService,
|
|
648
656
|
ModelMetadataHelperService,
|
|
649
657
|
FieldMetadataService,
|
|
@@ -656,6 +664,8 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
|
|
|
656
664
|
InfoService,
|
|
657
665
|
SolidIntrospectService,
|
|
658
666
|
DiscoveryService,
|
|
667
|
+
DraftPublishHelperService,
|
|
668
|
+
InternationalisationHelperService,
|
|
659
669
|
CrudHelperService,
|
|
660
670
|
CRUDService,
|
|
661
671
|
Reflector,
|
|
@@ -976,6 +986,8 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
|
|
|
976
986
|
ChatterMessageRepository,
|
|
977
987
|
ChatterMessageService,
|
|
978
988
|
ConfigModule,
|
|
989
|
+
DraftPublishHelperService,
|
|
990
|
+
InternationalisationHelperService,
|
|
979
991
|
CrudHelperService,
|
|
980
992
|
CRUDService,
|
|
981
993
|
CsvService,
|
|
@@ -998,6 +1010,7 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
|
|
|
998
1010
|
SmsFactory,
|
|
999
1011
|
MediaService,
|
|
1000
1012
|
MediaStorageProviderMetadataService,
|
|
1013
|
+
MetadataValidationService,
|
|
1001
1014
|
ModelMetadataHelperService,
|
|
1002
1015
|
ModelMetadataService,
|
|
1003
1016
|
ModuleMetadataService,
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const test_1 = require("@playwright/test");
|
|
4
|
-
const env_1 = require("../helpers/env");
|
|
5
|
-
const baseURL = process.env.API_BASE_URL ?? "http://localhost:3000";
|
|
6
|
-
const TEST_USER_EMAIL = (0, env_1.getRequiredEnv)("TEST_USER_EMAIL");
|
|
7
|
-
const TEST_USER_PASSWORD = (0, env_1.getRequiredEnv)("TEST_USER_PASSWORD");
|
|
8
|
-
function base64UrlDecode(input) {
|
|
9
|
-
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
10
|
-
const padded = normalized.length % 4 === 0
|
|
11
|
-
? normalized
|
|
12
|
-
: normalized.padEnd(normalized.length + (4 - (normalized.length % 4)), "=");
|
|
13
|
-
return Buffer.from(padded, "base64").toString("utf-8");
|
|
14
|
-
}
|
|
15
|
-
function validateJwt(token) {
|
|
16
|
-
const parts = token.split(".");
|
|
17
|
-
if (parts.length !== 3) {
|
|
18
|
-
throw new Error("JWT must have three dot-separated parts.");
|
|
19
|
-
}
|
|
20
|
-
const headerJson = JSON.parse(base64UrlDecode(parts[0]));
|
|
21
|
-
const payloadJson = JSON.parse(base64UrlDecode(parts[1]));
|
|
22
|
-
if (!headerJson || typeof headerJson !== "object") {
|
|
23
|
-
throw new Error("JWT header must be a JSON object.");
|
|
24
|
-
}
|
|
25
|
-
if (!payloadJson || typeof payloadJson !== "object") {
|
|
26
|
-
throw new Error("JWT payload must be a JSON object.");
|
|
27
|
-
}
|
|
28
|
-
if (typeof payloadJson.exp !== "number") {
|
|
29
|
-
throw new Error("JWT payload.exp must be a number.");
|
|
30
|
-
}
|
|
31
|
-
return payloadJson;
|
|
32
|
-
}
|
|
33
|
-
(0, test_1.test)("API: authenticate succeeds with valid credentials", async () => {
|
|
34
|
-
const api = await test_1.request.newContext({
|
|
35
|
-
baseURL,
|
|
36
|
-
extraHTTPHeaders: {
|
|
37
|
-
accept: "*/*",
|
|
38
|
-
"content-type": "application/json",
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
try {
|
|
42
|
-
const res = await api.post("/api/iam/authenticate", {
|
|
43
|
-
data: {
|
|
44
|
-
email: TEST_USER_EMAIL,
|
|
45
|
-
username: "",
|
|
46
|
-
password: TEST_USER_PASSWORD,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
(0, test_1.expect)(res.status()).toBe(200);
|
|
50
|
-
const json = await res.json();
|
|
51
|
-
(0, test_1.expect)(json.statusCode).toBe(200);
|
|
52
|
-
(0, test_1.expect)(Array.isArray(json.message)).toBe(true);
|
|
53
|
-
(0, test_1.expect)(json.message.length).toBe(0);
|
|
54
|
-
(0, test_1.expect)(json.error).toBe("");
|
|
55
|
-
const user = json.data?.user;
|
|
56
|
-
(0, test_1.expect)(user, "Expected data.user to be an object.").toBeTruthy();
|
|
57
|
-
(0, test_1.expect)(typeof user).toBe("object");
|
|
58
|
-
const email = user?.email;
|
|
59
|
-
(0, test_1.expect)(typeof email).toBe("string");
|
|
60
|
-
if (email === TEST_USER_EMAIL) {
|
|
61
|
-
(0, test_1.expect)(email).toBe(TEST_USER_EMAIL);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
(0, test_1.expect)(email.length).toBeGreaterThan(0);
|
|
65
|
-
}
|
|
66
|
-
(0, test_1.expect)(typeof user?.mobile).toBe("string");
|
|
67
|
-
(0, test_1.expect)(typeof user?.username).toBe("string");
|
|
68
|
-
(0, test_1.expect)(typeof user?.forcePasswordChange).toBe("boolean");
|
|
69
|
-
(0, test_1.expect)(typeof user?.id).toBe("number");
|
|
70
|
-
const roles = user?.roles;
|
|
71
|
-
(0, test_1.expect)(Array.isArray(roles)).toBe(true);
|
|
72
|
-
if (Array.isArray(roles)) {
|
|
73
|
-
(0, test_1.expect)(roles.every((role) => typeof role === "string")).toBe(true);
|
|
74
|
-
(0, test_1.expect)(roles).toContain("Admin");
|
|
75
|
-
}
|
|
76
|
-
const accessToken = json.data?.accessToken;
|
|
77
|
-
const refreshToken = json.data?.refreshToken;
|
|
78
|
-
(0, test_1.expect)(typeof accessToken).toBe("string");
|
|
79
|
-
(0, test_1.expect)(typeof refreshToken).toBe("string");
|
|
80
|
-
const accessPayload = validateJwt(accessToken);
|
|
81
|
-
const refreshPayload = validateJwt(refreshToken);
|
|
82
|
-
(0, test_1.expect)(typeof accessPayload.exp).toBe("number");
|
|
83
|
-
(0, test_1.expect)(typeof refreshPayload.exp).toBe("number");
|
|
84
|
-
}
|
|
85
|
-
finally {
|
|
86
|
-
await api.dispose();
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
(0, test_1.test)("API: authenticate fails with wrong password", async () => {
|
|
90
|
-
const api = await test_1.request.newContext({
|
|
91
|
-
baseURL,
|
|
92
|
-
extraHTTPHeaders: {
|
|
93
|
-
accept: "*/*",
|
|
94
|
-
"content-type": "application/json",
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
try {
|
|
98
|
-
const res = await api.post("/api/iam/authenticate", {
|
|
99
|
-
data: {
|
|
100
|
-
email: TEST_USER_EMAIL,
|
|
101
|
-
username: "",
|
|
102
|
-
password: `${TEST_USER_PASSWORD}__wrong`,
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
(0, test_1.expect)(res.status()).toBe(401);
|
|
106
|
-
const json = await res.json();
|
|
107
|
-
(0, test_1.expect)(json.statusCode).toBe(401);
|
|
108
|
-
(0, test_1.expect)(json.statusCodeMessage).toBe("Unauthorized");
|
|
109
|
-
(0, test_1.expect)(json.message).toBe("Invalid credentials");
|
|
110
|
-
(0, test_1.expect)(json.error).toBe("Invalid credentials");
|
|
111
|
-
(0, test_1.expect)(json.data?.statusCode).toBe(401);
|
|
112
|
-
(0, test_1.expect)(json.data?.error).toBe("Unauthorized");
|
|
113
|
-
(0, test_1.expect)(json.data?.message).toBe("Invalid credentials");
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
await api.dispose();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
//# sourceMappingURL=authenticate.spec.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"authenticate.spec.js","sourceRoot":"","sources":["../../tests/api/authenticate.spec.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, request, test } from \"@playwright/test\";\nimport { getRequiredEnv } from \"../helpers/env\";\n\ntype JwtPayload = {\n exp?: number;\n [key: string]: unknown;\n};\n\nconst baseURL = process.env.API_BASE_URL ?? \"http://localhost:3000\";\nconst TEST_USER_EMAIL = getRequiredEnv(\"TEST_USER_EMAIL\");\nconst TEST_USER_PASSWORD = getRequiredEnv(\"TEST_USER_PASSWORD\");\n\nfunction base64UrlDecode(input: string): string {\n const normalized = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded =\n normalized.length % 4 === 0\n ? normalized\n : normalized.padEnd(\n normalized.length + (4 - (normalized.length % 4)),\n \"=\"\n );\n return Buffer.from(padded, \"base64\").toString(\"utf-8\");\n}\n\nfunction validateJwt(token: string): JwtPayload {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n throw new Error(\"JWT must have three dot-separated parts.\");\n }\n\n const headerJson = JSON.parse(base64UrlDecode(parts[0]));\n const payloadJson = JSON.parse(base64UrlDecode(parts[1]));\n\n if (!headerJson || typeof headerJson !== \"object\") {\n throw new Error(\"JWT header must be a JSON object.\");\n }\n\n if (!payloadJson || typeof payloadJson !== \"object\") {\n throw new Error(\"JWT payload must be a JSON object.\");\n }\n\n if (typeof payloadJson.exp !== \"number\") {\n throw new Error(\"JWT payload.exp must be a number.\");\n }\n\n return payloadJson as JwtPayload;\n}\n\ntest(\"API: authenticate succeeds with valid credentials\", async () => {\n const api = await request.newContext({\n baseURL,\n extraHTTPHeaders: {\n accept: \"*/*\",\n \"content-type\": \"application/json\",\n },\n });\n\n try {\n const res = await api.post(\"/api/iam/authenticate\", {\n data: {\n email: TEST_USER_EMAIL,\n username: \"\",\n password: TEST_USER_PASSWORD,\n },\n });\n\n expect(res.status()).toBe(200);\n const json = await res.json();\n\n expect(json.statusCode).toBe(200);\n expect(Array.isArray(json.message)).toBe(true);\n expect(json.message.length).toBe(0);\n expect(json.error).toBe(\"\");\n\n const user = json.data?.user as Record<string, unknown> | undefined;\n expect(user, \"Expected data.user to be an object.\").toBeTruthy();\n expect(typeof user).toBe(\"object\");\n\n const email = user?.email;\n expect(typeof email).toBe(\"string\");\n if (email === TEST_USER_EMAIL) {\n expect(email).toBe(TEST_USER_EMAIL);\n } else {\n // If your API returns a fixed system email, replace this with an exact match.\n expect((email as string).length).toBeGreaterThan(0);\n }\n\n expect(typeof user?.mobile).toBe(\"string\");\n expect(typeof user?.username).toBe(\"string\");\n expect(typeof user?.forcePasswordChange).toBe(\"boolean\");\n expect(typeof user?.id).toBe(\"number\");\n\n const roles = user?.roles;\n expect(Array.isArray(roles)).toBe(true);\n if (Array.isArray(roles)) {\n expect(roles.every((role) => typeof role === \"string\")).toBe(true);\n expect(roles).toContain(\"Admin\");\n }\n\n const accessToken = json.data?.accessToken;\n const refreshToken = json.data?.refreshToken;\n expect(typeof accessToken).toBe(\"string\");\n expect(typeof refreshToken).toBe(\"string\");\n\n const accessPayload = validateJwt(accessToken as string);\n const refreshPayload = validateJwt(refreshToken as string);\n\n expect(typeof accessPayload.exp).toBe(\"number\");\n expect(typeof refreshPayload.exp).toBe(\"number\");\n } finally {\n await api.dispose();\n }\n});\n\ntest(\"API: authenticate fails with wrong password\", async () => {\n const api = await request.newContext({\n baseURL,\n extraHTTPHeaders: {\n accept: \"*/*\",\n \"content-type\": \"application/json\",\n },\n });\n\n try {\n const res = await api.post(\"/api/iam/authenticate\", {\n data: {\n email: TEST_USER_EMAIL,\n username: \"\",\n password: `${TEST_USER_PASSWORD}__wrong`,\n },\n });\n\n expect(res.status()).toBe(401);\n const json = await res.json();\n\n expect(json.statusCode).toBe(401);\n expect(json.statusCodeMessage).toBe(\"Unauthorized\");\n expect(json.message).toBe(\"Invalid credentials\");\n expect(json.error).toBe(\"Invalid credentials\");\n expect(json.data?.statusCode).toBe(401);\n expect(json.data?.error).toBe(\"Unauthorized\");\n expect(json.data?.message).toBe(\"Invalid credentials\");\n } finally {\n await api.dispose();\n }\n});\n"]}
|