Backfill package json for custom and standard app (#17681)
# Backfill application package files for custom and standard apps - Backfill `package.json` / `yarn.lock` (and related fields) for existing workspaces; new standard/custom apps get default dependency files. - Default package files under `application/constants/default-package-files/`; util with hardcoded checksums (comment on how to regenerate). - New **FileFolder.Dependencies** for app dependency files; **writeFile_v2** accepts optional `queryRunner` for transactional writes. Usages: - **Upgrade command** `upgrade:1-17:backfill-application-package-files`: standard/custom apps → default files; other apps → from logic function layer. - **Workspace creation**: create workspace with `workspaceCustomApplicationId` first, then create application (enables same-transaction insert). Migration makes workspace/application/file FKs deferrable. - dev seeder
This commit is contained in:
@@ -1567,6 +1567,7 @@ export enum FileFolder {
|
||||
Attachment = 'Attachment',
|
||||
BuiltFrontComponent = 'BuiltFrontComponent',
|
||||
BuiltLogicFunction = 'BuiltLogicFunction',
|
||||
Dependencies = 'Dependencies',
|
||||
File = 'File',
|
||||
FilesField = 'FilesField',
|
||||
PersonPicture = 'PersonPicture',
|
||||
|
||||
@@ -1539,6 +1539,7 @@ export enum FileFolder {
|
||||
Attachment = 'Attachment',
|
||||
BuiltFrontComponent = 'BuiltFrontComponent',
|
||||
BuiltLogicFunction = 'BuiltLogicFunction',
|
||||
Dependencies = 'Dependencies',
|
||||
File = 'File',
|
||||
FilesField = 'FilesField',
|
||||
PersonPicture = 'PersonPicture',
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"watchOptions": {
|
||||
"ignored": ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.nx/**"]
|
||||
"ignored": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/.git/**",
|
||||
"**/.nx/**"
|
||||
]
|
||||
},
|
||||
"compilerOptions": {
|
||||
"builder": "swc",
|
||||
@@ -37,6 +42,10 @@
|
||||
{
|
||||
"include": "engine/workspace-manager/dev-seeder/data/sample-files/**",
|
||||
"outDir": "dist/assets"
|
||||
},
|
||||
{
|
||||
"include": "engine/core-modules/application/constants/default-package-files/**",
|
||||
"outDir": "dist/assets"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { parseAvailablePackagesFromPackageJsonAndYarnLock } from 'src/engine/core-modules/application/utils/parse-available-packages-from-package-json-and-yarn-lock.util';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileSettings } from 'src/engine/core-modules/file/types/file-settings.types';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { LogicFunctionLayerEntity } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.entity';
|
||||
import { logicFunctionCreateHash } from 'src/engine/metadata-modules/logic-function/utils/logic-function-create-hash.utils';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-17:backfill-application-package-files',
|
||||
description:
|
||||
'Backfill application package files: standard/custom apps get default files, other apps get files from logic function layer',
|
||||
})
|
||||
export class BackfillApplicationPackageFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
protected readonly applicationService: ApplicationService,
|
||||
protected readonly fileStorageService: FileStorageService,
|
||||
protected readonly workspaceCacheService: WorkspaceCacheService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running backfill application package files for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const dryRun = options.dryRun ?? false;
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
select: ['id', 'workspaceCustomApplicationId'],
|
||||
});
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
this.logger.warn(`Workspace ${workspaceId} not found, skipping`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatApplicationMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
const flatApplications = Object.values(
|
||||
flatApplicationMaps.byId,
|
||||
) as FlatApplication[];
|
||||
|
||||
for (const application of flatApplications) {
|
||||
const isStandardOrCustomApplication =
|
||||
application.id === twentyStandardFlatApplication.id ||
|
||||
application.id === workspaceCustomFlatApplication.id;
|
||||
|
||||
if (isStandardOrCustomApplication) {
|
||||
const needsBackfill =
|
||||
!isNonEmptyString(application.packageJsonFileId) ||
|
||||
!isNonEmptyString(application.yarnLockFileId) ||
|
||||
!isNonEmptyString(application.packageJsonChecksum) ||
|
||||
!isNonEmptyString(application.yarnLockChecksum) ||
|
||||
!isNonEmptyString(application.availablePackages);
|
||||
|
||||
if (needsBackfill) {
|
||||
this.logger.log(
|
||||
`Backfilling standard/custom application ${application.id} with default package files`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.applicationService.uploadDefaultPackageFilesAndSetFileIds(
|
||||
application,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Skipping standard/custom application ${application.id} - already has package files`,
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasLogicFunctionLayer = isDefined(application.logicFunctionLayerId);
|
||||
const needsBackfill =
|
||||
!isNonEmptyString(application.packageJsonFileId) ||
|
||||
!isNonEmptyString(application.yarnLockFileId) ||
|
||||
!isNonEmptyString(application.packageJsonChecksum) ||
|
||||
!isNonEmptyString(application.yarnLockChecksum) ||
|
||||
!isNonEmptyString(application.availablePackages);
|
||||
|
||||
if (!hasLogicFunctionLayer) {
|
||||
this.logger.log(
|
||||
`Skipping application ${application.id} - no logic function layer`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!needsBackfill) {
|
||||
this.logger.log(
|
||||
`Skipping application ${application.id} - already has package files`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Backfilling application ${application.id} from logic function layer ${application.logicFunctionLayerId}`,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await this.backfillApplicationFromLogicFunctionLayer(application);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async backfillApplicationFromLogicFunctionLayer(
|
||||
application: Pick<
|
||||
FlatApplication,
|
||||
'id' | 'universalIdentifier' | 'workspaceId' | 'logicFunctionLayerId'
|
||||
>,
|
||||
): Promise<void> {
|
||||
const layerRepository = this.coreDataSource.getRepository(
|
||||
LogicFunctionLayerEntity,
|
||||
);
|
||||
const layer = await layerRepository.findOne({
|
||||
where: {
|
||||
id: application.logicFunctionLayerId as string,
|
||||
workspaceId: application.workspaceId,
|
||||
},
|
||||
select: ['id', 'packageJson', 'yarnLock'],
|
||||
});
|
||||
|
||||
if (!isDefined(layer)) {
|
||||
this.logger.warn(
|
||||
`Logic function layer ${application.logicFunctionLayerId} not found for application ${application.id}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const packageJsonContent = JSON.stringify(layer.packageJson, null, 2);
|
||||
const packageJsonChecksum = logicFunctionCreateHash(
|
||||
JSON.stringify(layer.packageJson),
|
||||
);
|
||||
const yarnLockChecksum = logicFunctionCreateHash(layer.yarnLock);
|
||||
const availablePackages = parseAvailablePackagesFromPackageJsonAndYarnLock(
|
||||
packageJsonContent,
|
||||
layer.yarnLock,
|
||||
);
|
||||
|
||||
const dependencyFileSettings: FileSettings = {
|
||||
isTemporaryFile: false,
|
||||
toDelete: false,
|
||||
};
|
||||
|
||||
const packageJsonFile = await this.fileStorageService.writeFile_v2({
|
||||
sourceFile: packageJsonContent,
|
||||
mimeType: undefined,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId: application.workspaceId,
|
||||
resourcePath: 'package.json',
|
||||
settings: dependencyFileSettings,
|
||||
});
|
||||
|
||||
const yarnLockFile = await this.fileStorageService.writeFile_v2({
|
||||
sourceFile: layer.yarnLock,
|
||||
mimeType: undefined,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId: application.workspaceId,
|
||||
resourcePath: 'yarn.lock',
|
||||
settings: dependencyFileSettings,
|
||||
});
|
||||
|
||||
await this.applicationService.update(application.id, {
|
||||
packageJsonFileId: packageJsonFile.id,
|
||||
yarnLockFileId: yarnLockFile.id,
|
||||
packageJsonChecksum,
|
||||
yarnLockChecksum,
|
||||
availablePackages,
|
||||
workspaceId: application.workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillApplicationPackageFilesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command';
|
||||
import { DeleteFileRecordsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files.command';
|
||||
import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command';
|
||||
import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
@@ -64,6 +65,7 @@ import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objec
|
||||
MigrateSendEmailRecipientsCommand,
|
||||
MigrateWorkflowCodeStepsCommand,
|
||||
SeedWorkflowV1_16Command,
|
||||
BackfillApplicationPackageFilesCommand,
|
||||
],
|
||||
exports: [
|
||||
MigrateAttachmentToMorphRelationsCommand,
|
||||
@@ -73,6 +75,7 @@ import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objec
|
||||
MigrateSendEmailRecipientsCommand,
|
||||
MigrateWorkflowCodeStepsCommand,
|
||||
SeedWorkflowV1_16Command,
|
||||
BackfillApplicationPackageFilesCommand,
|
||||
],
|
||||
})
|
||||
export class V1_17_UpgradeVersionCommandModule {}
|
||||
|
||||
+3
@@ -9,6 +9,7 @@ import {
|
||||
UpgradeCommandRunner,
|
||||
type VersionCommands,
|
||||
} from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||
import { BackfillApplicationPackageFilesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-backfill-application-package-files.command';
|
||||
import { DeleteFileRecordsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-delete-all-files.command';
|
||||
import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command';
|
||||
import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
@@ -34,6 +35,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
|
||||
// 1.17 Commands
|
||||
protected readonly backfillApplicationPackageFilesCommand: BackfillApplicationPackageFilesCommand,
|
||||
protected readonly deleteFileRecordsCommand: DeleteFileRecordsCommand,
|
||||
protected readonly migrateAttachmentToMorphRelationsCommand: MigrateAttachmentToMorphRelationsCommand,
|
||||
protected readonly identifyWebhookMetadataCommand: IdentifyWebhookMetadataCommand,
|
||||
@@ -57,6 +59,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
.makeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this.migrateWorkflowCodeStepsCommand,
|
||||
this.deleteFileRecordsCommand,
|
||||
this.backfillApplicationPackageFilesCommand,
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class MakeWorkspaceAndApplicationFileFksDeferrable1770050200000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'MakeWorkspaceAndApplicationFileFksDeferrable1770050200000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755" FOREIGN KEY ("workspaceCustomApplicationId") REFERENCES "core"."application"("id") ON DELETE RESTRICT ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" DROP CONSTRAINT "FK_28f20711184b3c3318a8e44d117"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" DROP CONSTRAINT "FK_3818380258798f9ffa9963b6dc4"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ADD CONSTRAINT "FK_3818380258798f9ffa9963b6dc4" FOREIGN KEY ("packageJsonFileId") REFERENCES "core"."file"("id") ON DELETE RESTRICT ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ADD CONSTRAINT "FK_28f20711184b3c3318a8e44d117" FOREIGN KEY ("yarnLockFileId") REFERENCES "core"."file"("id") ON DELETE RESTRICT ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" DROP CONSTRAINT "FK_28f20711184b3c3318a8e44d117"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" DROP CONSTRAINT "FK_3818380258798f9ffa9963b6dc4"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ADD CONSTRAINT "FK_3818380258798f9ffa9963b6dc4" FOREIGN KEY ("packageJsonFileId") REFERENCES "core"."file"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."application" ADD CONSTRAINT "FK_28f20711184b3c3318a8e44d117" FOREIGN KEY ("yarnLockFileId") REFERENCES "core"."file"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD CONSTRAINT "FK_3b1acb13a5dac9956d1a4b32755" FOREIGN KEY ("workspaceCustomApplicationId") REFERENCES "core"."application"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
"@types/lodash.camelcase": "^4.3.9",
|
||||
"@types/lodash.compact": "^3.0.9",
|
||||
"@types/lodash.groupby": "^4.6.9",
|
||||
"@types/lodash.identity": "^3.0.9",
|
||||
"@types/lodash.isempty": "^4.4.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.isobject": "^3.0.9",
|
||||
"@types/lodash.kebabcase": "^4.1.9",
|
||||
"@types/lodash.mapvalues": "^4.6.9",
|
||||
"@types/lodash.omit": "^4.5.9",
|
||||
"@types/lodash.pickby": "^4.6.9",
|
||||
"@types/lodash.snakecase": "^4.1.9",
|
||||
"@types/lodash.upperfirst": "^4.3.9",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.12.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.4",
|
||||
"deep-equal": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"lodash.compact": "^3.0.1",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.identity": "^3.0.0",
|
||||
"lodash.isempty": "^4.4.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.isobject": "^3.0.2",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.mapvalues": "^4.6.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"lodash.upperfirst": "^4.3.1",
|
||||
"nodemailer": "^7.0.11",
|
||||
"sharp": "^0.33.5",
|
||||
"uuid": "^10.0.0",
|
||||
"winston": "^3.14.2"
|
||||
}
|
||||
}
|
||||
+3374
File diff suppressed because it is too large
Load Diff
+24
-8
@@ -23,7 +23,9 @@ import {
|
||||
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { getDefaultApplicationPackageFields } from 'src/engine/core-modules/application/utils/get-default-application-package-fields.util';
|
||||
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { LogicFunctionLayerService } from 'src/engine/core-modules/logic-function/logic-function-layer/services/logic-function-layer.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
@@ -43,7 +45,6 @@ import { PermissionFlagService } from 'src/engine/metadata-modules/permission-fl
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
@Injectable()
|
||||
@@ -147,12 +148,15 @@ export class ApplicationSyncService {
|
||||
).toString('utf-8'),
|
||||
) as PackageJson;
|
||||
|
||||
const application =
|
||||
(await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
})) ??
|
||||
(await this.applicationService.create({
|
||||
const defaultPackageFields = await getDefaultApplicationPackageFields();
|
||||
|
||||
let application = await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!application) {
|
||||
const created = await this.applicationService.create({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
name,
|
||||
description: manifest.application.description,
|
||||
@@ -161,7 +165,19 @@ export class ApplicationSyncService {
|
||||
logicFunctionLayerId: null,
|
||||
defaultRoleId: null,
|
||||
workspaceId,
|
||||
}));
|
||||
packageJsonChecksum: defaultPackageFields.packageJsonChecksum,
|
||||
packageJsonFileId: null,
|
||||
yarnLockChecksum: defaultPackageFields.yarnLockChecksum,
|
||||
yarnLockFileId: null,
|
||||
availablePackages: defaultPackageFields.availablePackages,
|
||||
});
|
||||
|
||||
await this.applicationService.uploadDefaultPackageFilesAndSetFileIds(
|
||||
created,
|
||||
);
|
||||
|
||||
application = created;
|
||||
}
|
||||
|
||||
let logicFunctionLayerId = application.logicFunctionLayerId;
|
||||
|
||||
|
||||
+99
-3
@@ -1,17 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner, type Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { getDefaultApplicationPackageFields } from 'src/engine/core-modules/application/utils/get-default-application-package-fields.util';
|
||||
import { parseAvailablePackagesFromPackageJsonAndYarnLock } from 'src/engine/core-modules/application/utils/parse-available-packages-from-package-json-and-yarn-lock.util';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant';
|
||||
import { logicFunctionCreateHash } from 'src/engine/metadata-modules/logic-function/utils/logic-function-create-hash.utils';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
@@ -21,6 +25,7 @@ export class ApplicationService {
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {}
|
||||
@@ -261,16 +266,28 @@ export class ApplicationService {
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const defaultPackageFields = await getDefaultApplicationPackageFields();
|
||||
|
||||
const twentyStandardApplication = await this.create(
|
||||
{
|
||||
...TWENTY_STANDARD_APPLICATION,
|
||||
logicFunctionLayerId: null,
|
||||
workspaceId,
|
||||
canBeUninstalled: false,
|
||||
packageJsonChecksum: defaultPackageFields.packageJsonChecksum,
|
||||
packageJsonFileId: null,
|
||||
yarnLockChecksum: defaultPackageFields.yarnLockChecksum,
|
||||
yarnLockFileId: null,
|
||||
availablePackages: defaultPackageFields.availablePackages,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.uploadDefaultPackageFilesAndSetFileIds(
|
||||
twentyStandardApplication,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (!skipCacheInvalidation) {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatApplicationMaps',
|
||||
@@ -283,14 +300,17 @@ export class ApplicationService {
|
||||
async createWorkspaceCustomApplication(
|
||||
{
|
||||
workspaceId,
|
||||
applicationId,
|
||||
workspaceDisplayName,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
workspaceDisplayName?: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const applicationId = v4();
|
||||
const defaultPackageFields = await getDefaultApplicationPackageFields();
|
||||
|
||||
const workspaceCustomApplication = await this.create(
|
||||
{
|
||||
description: 'Workspace custom application',
|
||||
@@ -298,17 +318,93 @@ export class ApplicationService {
|
||||
sourcePath: 'workspace-custom',
|
||||
version: '1.0.0',
|
||||
universalIdentifier: applicationId,
|
||||
workspaceId: workspaceId,
|
||||
workspaceId,
|
||||
id: applicationId,
|
||||
logicFunctionLayerId: null,
|
||||
canBeUninstalled: false,
|
||||
packageJsonChecksum: defaultPackageFields.packageJsonChecksum,
|
||||
packageJsonFileId: null,
|
||||
yarnLockChecksum: defaultPackageFields.yarnLockChecksum,
|
||||
yarnLockFileId: null,
|
||||
availablePackages: defaultPackageFields.availablePackages,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.uploadDefaultPackageFilesAndSetFileIds(
|
||||
workspaceCustomApplication,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return workspaceCustomApplication;
|
||||
}
|
||||
|
||||
async uploadDefaultPackageFilesAndSetFileIds(
|
||||
application: Pick<
|
||||
ApplicationEntity,
|
||||
'id' | 'universalIdentifier' | 'workspaceId'
|
||||
>,
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<void> {
|
||||
const defaultPackageFields = await getDefaultApplicationPackageFields();
|
||||
|
||||
const packageJsonChecksum = logicFunctionCreateHash(
|
||||
JSON.stringify(JSON.parse(defaultPackageFields.packageJsonContent)),
|
||||
);
|
||||
const yarnLockChecksum = logicFunctionCreateHash(
|
||||
defaultPackageFields.yarnLockContent,
|
||||
);
|
||||
const availablePackages = parseAvailablePackagesFromPackageJsonAndYarnLock(
|
||||
defaultPackageFields.packageJsonContent,
|
||||
defaultPackageFields.yarnLockContent,
|
||||
);
|
||||
|
||||
const packageJsonFile = await this.fileStorageService.writeFile_v2({
|
||||
sourceFile: defaultPackageFields.packageJsonContent,
|
||||
mimeType: undefined,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId: application.workspaceId,
|
||||
resourcePath: 'package.json',
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
const yarnLockFile = await this.fileStorageService.writeFile_v2({
|
||||
sourceFile: defaultPackageFields.yarnLockContent,
|
||||
mimeType: undefined,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId: application.workspaceId,
|
||||
resourcePath: 'yarn.lock',
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
if (queryRunner) {
|
||||
await queryRunner.manager.update(
|
||||
ApplicationEntity,
|
||||
{ id: application.id },
|
||||
{
|
||||
packageJsonFileId: packageJsonFile.id,
|
||||
yarnLockFileId: yarnLockFile.id,
|
||||
packageJsonChecksum,
|
||||
yarnLockChecksum,
|
||||
availablePackages,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await this.update(application.id, {
|
||||
packageJsonFileId: packageJsonFile.id,
|
||||
yarnLockFileId: yarnLockFile.id,
|
||||
packageJsonChecksum,
|
||||
yarnLockChecksum,
|
||||
availablePackages,
|
||||
workspaceId: application.workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<ApplicationEntity> & { workspaceId: string },
|
||||
queryRunner?: QueryRunner,
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { ASSET_PATH } from 'src/constants/assets-path';
|
||||
import { parseAvailablePackagesFromPackageJsonAndYarnLock } from 'src/engine/core-modules/application/utils/parse-available-packages-from-package-json-and-yarn-lock.util';
|
||||
|
||||
const DEFAULT_PACKAGE_FILES_DIR = path.join(
|
||||
ASSET_PATH,
|
||||
'engine/core-modules/application/constants/default-package-files',
|
||||
);
|
||||
|
||||
// To regenerate: use logicFunctionCreateHash from logic-function-create-hash.utils.
|
||||
// package.json: hash(JSON.stringify(JSON.parse(content))). yarn.lock: hash(content).
|
||||
// Both use first 32 chars of SHA512 hex digest.
|
||||
const DEFAULT_PACKAGE_JSON_CHECKSUM = '84772134573e316a4eb1f9dc2e58706a';
|
||||
const DEFAULT_YARN_LOCK_CHECKSUM = '8d837b7503cf8eff21c6446a126588d4';
|
||||
|
||||
export type DefaultApplicationPackageFields = {
|
||||
packageJsonChecksum: string;
|
||||
yarnLockChecksum: string;
|
||||
availablePackages: Record<string, string>;
|
||||
packageJsonContent: string;
|
||||
yarnLockContent: string;
|
||||
};
|
||||
|
||||
export const getDefaultApplicationPackageFields =
|
||||
async (): Promise<DefaultApplicationPackageFields> => {
|
||||
const [packageJsonContent, yarnLockContent] = await Promise.all([
|
||||
readFile(path.join(DEFAULT_PACKAGE_FILES_DIR, 'package.json'), 'utf8'),
|
||||
readFile(path.join(DEFAULT_PACKAGE_FILES_DIR, 'yarn.lock'), 'utf8'),
|
||||
]);
|
||||
|
||||
const availablePackages = parseAvailablePackagesFromPackageJsonAndYarnLock(
|
||||
packageJsonContent,
|
||||
yarnLockContent,
|
||||
);
|
||||
|
||||
return {
|
||||
packageJsonChecksum: DEFAULT_PACKAGE_JSON_CHECKSUM,
|
||||
yarnLockChecksum: DEFAULT_YARN_LOCK_CHECKSUM,
|
||||
availablePackages,
|
||||
packageJsonContent,
|
||||
yarnLockContent,
|
||||
};
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { type PackageJson } from 'type-fest';
|
||||
|
||||
const PACKAGE_VERSION_REGEX =
|
||||
/^"(@?[^@]+(?:\/[^@]+)?)@.*?":\n\s+version:\s*(.+)$/gm;
|
||||
|
||||
const MAX_PACKAGE_VERSION_MATCHES = 1_000;
|
||||
|
||||
export const parseAvailablePackagesFromPackageJsonAndYarnLock = (
|
||||
packageJsonContent: string,
|
||||
yarnLockContent: string,
|
||||
): Record<string, string> => {
|
||||
const packageJson = JSON.parse(packageJsonContent) as PackageJson;
|
||||
const versions: Record<string, string> = {};
|
||||
let match: RegExpExecArray | null;
|
||||
let matchCount = 0;
|
||||
|
||||
while (
|
||||
matchCount < MAX_PACKAGE_VERSION_MATCHES &&
|
||||
(match = PACKAGE_VERSION_REGEX.exec(yarnLockContent)) !== null
|
||||
) {
|
||||
matchCount += 1;
|
||||
const packageName = match[1];
|
||||
const version = match[2];
|
||||
|
||||
if (packageJson.dependencies?.[packageName]) {
|
||||
versions[packageName] = version;
|
||||
}
|
||||
}
|
||||
|
||||
return versions;
|
||||
};
|
||||
@@ -465,26 +465,19 @@ export class SignInUpService {
|
||||
isWorkEmailFound && (await isLogoUrlValid()) ? logoUrl : undefined;
|
||||
|
||||
const workspaceId = v4();
|
||||
const workspaceCustomApplicationId = v4();
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const workspaceCustomApplication =
|
||||
await this.applicationService.createWorkspaceCustomApplication(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
const workspaceToCreate = this.workspaceRepository.create({
|
||||
id: workspaceId,
|
||||
subdomain: await this.subdomainManagerService.generateSubdomain(
|
||||
isWorkEmailFound ? { userEmail: email } : {},
|
||||
),
|
||||
workspaceCustomApplicationId: workspaceCustomApplication.id,
|
||||
workspaceCustomApplicationId,
|
||||
displayName: '',
|
||||
inviteHash: v4(),
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
@@ -496,6 +489,14 @@ export class SignInUpService {
|
||||
workspaceToCreate,
|
||||
);
|
||||
|
||||
await this.applicationService.createWorkspaceCustomApplication(
|
||||
{
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
const isExistingUser = userData.type === 'existingUser';
|
||||
const user = isExistingUser
|
||||
? userData.existingUser
|
||||
|
||||
+13
-4
@@ -7,7 +7,7 @@ import { type Readable } from 'stream';
|
||||
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { FileFolder, Sources } from 'twenty-shared/types';
|
||||
import { Like, Repository } from 'typeorm';
|
||||
import { Like, Repository, type QueryRunner } from 'typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
|
||||
@@ -69,15 +69,24 @@ export class FileStorageService {
|
||||
resourcePath,
|
||||
fileId,
|
||||
settings,
|
||||
queryRunner,
|
||||
}: ResourceIdentifier & {
|
||||
sourceFile: string | Buffer | Uint8Array;
|
||||
mimeType: string | undefined;
|
||||
fileId?: string;
|
||||
settings: FileSettings;
|
||||
queryRunner?: QueryRunner;
|
||||
}): Promise<FileEntity> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
const application = await this.applicationRepository.findOneOrFail({
|
||||
const applicationRepository = queryRunner
|
||||
? queryRunner.manager.getRepository(ApplicationEntity)
|
||||
: this.applicationRepository;
|
||||
const fileRepository = queryRunner
|
||||
? queryRunner.manager.getRepository(FileEntity)
|
||||
: this.fileRepository;
|
||||
|
||||
const application = await applicationRepository.findOneOrFail({
|
||||
where: {
|
||||
universalIdentifier: applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
@@ -97,7 +106,7 @@ export class FileStorageService {
|
||||
sourceFile,
|
||||
});
|
||||
|
||||
await this.fileRepository.upsert(
|
||||
await fileRepository.upsert(
|
||||
{
|
||||
path: `${fileFolder}/${resourcePath}`,
|
||||
workspaceId,
|
||||
@@ -112,7 +121,7 @@ export class FileStorageService {
|
||||
['path', 'workspaceId', 'applicationId'],
|
||||
);
|
||||
|
||||
return await this.fileRepository.findOneOrFail({
|
||||
return await fileRepository.findOneOrFail({
|
||||
where: {
|
||||
path: `${fileFolder}/${resourcePath}`,
|
||||
applicationId: application.id,
|
||||
|
||||
+3
@@ -45,6 +45,9 @@ export const fileFolderConfigs: Record<FileFolder, FileFolderConfig> = {
|
||||
[FileFolder.FilesField]: {
|
||||
ignoreExpirationToken: false,
|
||||
},
|
||||
[FileFolder.Dependencies]: {
|
||||
ignoreExpirationToken: false,
|
||||
},
|
||||
};
|
||||
|
||||
export type AllowedFolders = KebabCase<keyof typeof FileFolder>;
|
||||
|
||||
+12
-9
@@ -1,4 +1,5 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { seedBillingCustomers } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-customers.util';
|
||||
@@ -42,14 +43,7 @@ export const seedCoreSchema = async ({
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const customWorkspaceApplication =
|
||||
await applicationService.createWorkspaceCustomApplication(
|
||||
{
|
||||
workspaceId,
|
||||
workspaceDisplayName: createWorkspaceStaticInput.displayName,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
const workspaceCustomApplicationId = v4();
|
||||
|
||||
await createWorkspace({
|
||||
queryRunner,
|
||||
@@ -57,10 +51,19 @@ export const seedCoreSchema = async ({
|
||||
createWorkspaceInput: {
|
||||
...createWorkspaceStaticInput,
|
||||
version,
|
||||
workspaceCustomApplicationId: customWorkspaceApplication.id,
|
||||
workspaceCustomApplicationId,
|
||||
},
|
||||
});
|
||||
|
||||
await applicationService.createWorkspaceCustomApplication(
|
||||
{
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
workspaceDisplayName: createWorkspaceStaticInput.displayName,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await seedUsers({ queryRunner, schemaName });
|
||||
|
||||
await seedUserWorkspaces({ queryRunner, schemaName, workspaceId });
|
||||
|
||||
@@ -10,4 +10,5 @@ export enum FileFolder {
|
||||
PublicAsset = 'public-asset',
|
||||
Source = 'source',
|
||||
FilesField = 'files-field',
|
||||
Dependencies = 'dependencies',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user