diff --git a/.cspell.json b/.cspell.json index ff0761d6e07..66f9fa3f041 100644 --- a/.cspell.json +++ b/.cspell.json @@ -525,7 +525,17 @@ "xplatframework", "localforage", "longform", - "rfdc" + "rfdc", + "libx264", + "libx", + "WXGA", + "SXGA", + "UXGA", + "NTSC", + "libvpx", + "libaom", + "theora", + "Theora" ], "useGitignore": true, "ignorePaths": [ diff --git a/apps/api/config/custom-webpack.config.js b/apps/api/config/custom-webpack.config.js index 3bbe158fa41..f28665359dc 100644 --- a/apps/api/config/custom-webpack.config.js +++ b/apps/api/config/custom-webpack.config.js @@ -75,7 +75,7 @@ module.exports = composePlugins( ); // Log for debugging - console.log('Copy Patterns:', packagePatterns); + // console.log('Copy Patterns:', packagePatterns); // Log final config for debugging // console.log('Final Webpack config:', JSON.stringify(config, null, 2)); diff --git a/apps/api/package.json b/apps/api/package.json index e0f1f240e40..b5722287f21 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -53,6 +53,7 @@ "@gauzy/plugin-knowledge-base": "^0.1.0", "@gauzy/plugin-product-reviews": "^0.1.0", "@gauzy/plugin-sentry": "^0.1.0", + "@gauzy/plugin-video-capture": "^0.1.0", "dotenv": "^16.0.3", "yargs": "^17.5.0" }, diff --git a/apps/api/src/plugins.ts b/apps/api/src/plugins.ts index 20b2e2efc72..eb2a67df6ad 100644 --- a/apps/api/src/plugins.ts +++ b/apps/api/src/plugins.ts @@ -11,6 +11,7 @@ import { JobProposalPlugin } from '@gauzy/plugin-job-proposal'; import { JobSearchPlugin } from '@gauzy/plugin-job-search'; import { KnowledgeBasePlugin } from '@gauzy/plugin-knowledge-base'; import { ProductReviewsPlugin } from '@gauzy/plugin-product-reviews'; +import { VideoCapturePlugin } from '@gauzy/plugin-video-capture'; import { SentryTracing as SentryPlugin } from './sentry'; @@ -50,5 +51,7 @@ export const plugins = [ // Indicates the inclusion or intention to use the KnowledgeBasePlugin in the codebase. KnowledgeBasePlugin, // Indicates the inclusion or intention to use the ProductReviewsPlugin in the codebase. - ProductReviewsPlugin + ProductReviewsPlugin, + // Indicates the inclusion or intention to use the VideoCapturePlugin in the codebase. + VideoCapturePlugin ]; diff --git a/package.json b/package.json index e64b0284762..e4b7bec5959 100644 --- a/package.json +++ b/package.json @@ -156,9 +156,9 @@ "build:package:plugins:pre": "yarn run build:package:ui-core && yarn run build:package:ui-auth && yarn run build:package:plugin:onboarding-ui && yarn run build:package:plugin:legal-ui && yarn run build:package:plugin:job-search-ui && yarn run build:package:plugin:job-matching-ui && yarn run build:package:plugin:job-employee-ui && yarn run build:package:plugin:job-proposal-ui && yarn run build:package:plugin:public-layout-ui && yarn run build:package:plugin:maintenance-ui && yarn run build:integration-ui-plugins", "build:package:plugins:pre:prod": "yarn run build:package:ui-core:prod && yarn run build:package:ui-auth:prod && yarn run build:package:plugin:onboarding-ui:prod && yarn run build:package:plugin:legal-ui:prod && yarn run build:package:plugin:job-search-ui:prod && yarn run build:package:plugin:job-matching-ui:prod && yarn run build:package:plugin:job-employee-ui:prod && yarn run build:package:plugin:job-proposal-ui:prod && yarn run build:package:plugin:public-layout-ui:prod && yarn run build:package:plugin:maintenance-ui:prod && yarn run build:integration-ui-plugins:prod", "build:package:plugins:pre:docker": "yarn run build:package:ui-core:docker && yarn run build:package:ui-auth:docker && yarn run build:package:plugin:onboarding-ui:docker && yarn run build:package:plugin:legal-ui:docker && yarn run build:package:plugin:job-search-ui:docker && yarn run build:package:plugin:job-matching-ui:docker && yarn run build:package:plugin:job-employee-ui:docker && yarn run build:package:plugin:job-proposal-ui:docker && yarn run build:package:plugin:public-layout-ui:docker && yarn run build:package:plugin:maintenance-ui:docker && yarn run build:integration-ui-plugins:docker", - "build:package:plugins:post": "yarn run build:package:plugin:integration-jira && yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:sentry && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:job-search && yarn run build:package:plugin:job-proposal && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork", - "build:package:plugins:post:prod": "yarn run build:package:plugin:integration-jira:prod && yarn run build:package:plugin:integration-ai:prod && yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:job-search:prod && yarn run build:package:plugin:job-proposal:prod && yarn run build:package:plugin:integration-github:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod && yarn run build:package:plugin:integration-hubstaff:prod && yarn run build:package:plugin:integration-upwork:prod", - "build:package:plugins:post:docker": "yarn run build:package:plugin:integration-jira:docker && yarn run build:package:plugin:integration-ai:docker && yarn run build:package:plugin:sentry:docker && yarn run build:package:plugin:jitsu-analytic:docker && yarn run build:package:plugin:product-reviews:docker && yarn run build:package:plugin:job-search:docker && yarn run build:package:plugin:job-proposal:docker && yarn run build:package:plugin:integration-github:docker && yarn run build:package:plugin:knowledge-base:docker && yarn run build:package:plugin:changelog:docker && yarn run build:package:plugin:integration-hubstaff:docker && yarn run build:package:plugin:integration-upwork:docker", + "build:package:plugins:post": "yarn run build:package:plugin:integration-jira && yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:sentry && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:job-search && yarn run build:package:plugin:job-proposal && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork && yarn run build:package:plugin:video-capture", + "build:package:plugins:post:prod": "yarn run build:package:plugin:integration-jira:prod && yarn run build:package:plugin:integration-ai:prod && yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:job-search:prod && yarn run build:package:plugin:job-proposal:prod && yarn run build:package:plugin:integration-github:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod && yarn run build:package:plugin:integration-hubstaff:prod && yarn run build:package:plugin:integration-upwork:prod && yarn run build:package:plugin:video-capture:prod", + "build:package:plugins:post:docker": "yarn run build:package:plugin:integration-jira:docker && yarn run build:package:plugin:integration-ai:docker && yarn run build:package:plugin:sentry:docker && yarn run build:package:plugin:jitsu-analytic:docker && yarn run build:package:plugin:product-reviews:docker && yarn run build:package:plugin:job-search:docker && yarn run build:package:plugin:job-proposal:docker && yarn run build:package:plugin:integration-github:docker && yarn run build:package:plugin:knowledge-base:docker && yarn run build:package:plugin:changelog:docker && yarn run build:package:plugin:integration-hubstaff:docker && yarn run build:package:plugin:integration-upwork:docker && yarn run build:package:plugin:video-capture:docker", "build:package:plugin:integration-ai": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-integration-ai", "build:package:plugin:integration-ai:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-integration-ai", "build:package:plugin:integration-ai:docker": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=60000 yarn nx build plugin-integration-ai", @@ -231,6 +231,9 @@ "build:package:plugin:knowledge-base": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-knowledge-base", "build:package:plugin:knowledge-base:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-knowledge-base", "build:package:plugin:knowledge-base:docker": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=60000 yarn nx build plugin-knowledge-base", + "build:package:plugin:video-capture": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-video-capture", + "build:package:plugin:video-capture:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-video-capture", + "build:package:plugin:video-capture:docker": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=60000 yarn nx build plugin-video-capture", "build:package:plugin:changelog": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-changelog", "build:package:plugin:changelog:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn nx build plugin-changelog", "build:package:plugin:changelog:docker": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=60000 yarn nx build plugin-changelog", @@ -481,7 +484,7 @@ "husky": "^6.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jest-environment-node": "^29.4.1", + "jest-environment-node": "^29.7.0", "jest-jasmine2": "29.7.0", "jest-preset-angular": "14.1.1", "jsonc-eslint-parser": "^2.1.0", diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 46cb49220bf..948fc364450 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -4,6 +4,7 @@ export * from './lib/accounting-template.model'; /** App Setting Model */ export * from './lib/activity-log.model'; +export * from './lib/activity-watch.model'; export * from './lib/api-call-log.model'; export * from './lib/app.model'; export * from './lib/appointment-employees.model'; @@ -29,6 +30,7 @@ export * from './lib/core.model'; export * from './lib/country.model'; export * from './lib/currency.model'; export * from './lib/custom-smtp.model'; +export * from './lib/daily-plan.model'; export * from './lib/date-picker.model'; export * from './lib/dashboard.model'; export * from './lib/deal.model'; @@ -51,7 +53,6 @@ export * from './lib/equipment.model'; export * from './lib/estimate-email.model'; export * from './lib/event-type.model'; export * from './lib/expense-category.model'; -export * from './lib/expense-category.model'; export * from './lib/expense.model'; export * from './lib/favorite.model'; export * from './lib/feature.model'; @@ -67,8 +68,8 @@ export * from './lib/hubstaff.model'; export * from './lib/image-asset.model'; export * from './lib/import-export.model'; export * from './lib/income.model'; -export * from './lib/integration.model'; export * from './lib/integration-setting.model'; +export * from './lib/integration.model'; export * from './lib/invite.model'; export * from './lib/invoice-estimate-history.model'; export * from './lib/invoice-item.model'; @@ -87,8 +88,8 @@ export * from './lib/organization-employment-type.model'; export * from './lib/organization-expense-category.model'; export * from './lib/organization-language.model'; export * from './lib/organization-positions.model'; -export * from './lib/organization-projects.model'; export * from './lib/organization-project-module.model'; +export * from './lib/organization-projects.model'; export * from './lib/organization-recurring-expense.model'; export * from './lib/organization-sprint.model'; export * from './lib/organization-task-setting.model'; @@ -113,6 +114,7 @@ export * from './lib/request-approval.model'; export * from './lib/resource-link.model'; export * from './lib/role-permission.model'; export * from './lib/role.model'; +export * from './lib/screening-task.model'; export * from './lib/screenshot.model'; export * from './lib/seed.model'; export * from './lib/shared-types'; @@ -130,8 +132,6 @@ export * from './lib/task-status.model'; export * from './lib/task-version.model'; export * from './lib/task-view.model'; export * from './lib/task.model'; -export * from './lib/daily-plan.model'; -export * from './lib/screening-task.model'; export * from './lib/tenant.model'; export * from './lib/time-off.model'; export * from './lib/timesheet-statistics.model'; @@ -142,18 +142,17 @@ export * from './lib/upwork.model'; export * from './lib/user-organization.model'; export * from './lib/user.model'; export * from './lib/wakatime.model'; -export * from './lib/activity-watch.model'; export { + ActorTypeEnum, + BaseEntityEnum, IBaseEntityModel as BaseEntityModel, - ID, IBasePerTenantAndOrganizationEntityModel, IBasePerTenantEntityModel, - IBaseSoftDeleteEntityModel, IBaseRelationsEntityModel, - ActorTypeEnum, - JsonData, - BaseEntityEnum + IBaseSoftDeleteEntityModel, + ID, + JsonData } from './lib/base-entity.model'; export * from './lib/proxy.model'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c765857bfb5..d13742879ba 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,6 +13,8 @@ export { export * from './lib/logger'; export * from './lib/core'; export * from './lib/core/seeds'; +export { LazyFileInterceptor } from './lib/core/interceptors'; +export { FileStorage, FileStorageFactory, UploadedFileStorage } from './lib/core/file-storage'; export * from './lib/shared'; export * from './lib/event-bus'; diff --git a/packages/core/src/lib/core/entities/subscribers/index.ts b/packages/core/src/lib/core/entities/subscribers/index.ts index 1818e16c531..a713672a901 100644 --- a/packages/core/src/lib/core/entities/subscribers/index.ts +++ b/packages/core/src/lib/core/entities/subscribers/index.ts @@ -49,6 +49,7 @@ import { } from '../internal'; import { TenantOrganizationBaseEntityEventSubscriber } from './tenant-organization-base-entity.subscriber'; +// Get the ORM type from the MultiORMEnum const ormType = getORMType(); /** diff --git a/packages/core/src/lib/core/file-storage/helpers/directory-path-generator.interface.ts b/packages/core/src/lib/core/file-storage/helpers/directory-path-generator.interface.ts new file mode 100644 index 00000000000..7af3fb56087 --- /dev/null +++ b/packages/core/src/lib/core/file-storage/helpers/directory-path-generator.interface.ts @@ -0,0 +1,4 @@ +export interface IDirectoryPathGenerator { + getBaseDirectory(name: string): string; + getSubDirectory(): string; +} diff --git a/packages/core/src/lib/core/file-storage/helpers/directory-path-generator.ts b/packages/core/src/lib/core/file-storage/helpers/directory-path-generator.ts new file mode 100644 index 00000000000..7a89c9c8f92 --- /dev/null +++ b/packages/core/src/lib/core/file-storage/helpers/directory-path-generator.ts @@ -0,0 +1,37 @@ +// Concrete implementation of the path generator +import * as moment from 'moment'; +import * as path from 'path'; +import { v4 as uuid } from 'uuid'; +import { RequestContext } from '../../context'; +import { IDirectoryPathGenerator } from './directory-path-generator.interface'; + +export class DirectoryPathGenerator implements IDirectoryPathGenerator { + /** + * Generates the base directory path with the given name. + * Includes a timestamped subdirectory in the format `YYYY/MM/DD`. + * + * @param name - The name to be used as the base directory. + * @returns The full base directory path including the timestamped subdirectory. + */ + public getBaseDirectory(name: string): string { + return path.join(name, moment().format('YYYY/MM/DD')); + } + + /** + * Generates a subdirectory path specific to the current user context. + * Uses the `tenantId` and `employeeId` from the current user, or generates UUIDs if not available. + * + * @returns The subdirectory path in the format `/`. + */ + public getSubDirectory(): string { + // Retrieve the current user from the request context + const user = RequestContext.currentUser(); + + // Extract or generate identifiers for the tenant and employee + const tenantId = user?.tenantId || uuid(); // Use the tenantId if available, otherwise generate a UUID + const employeeId = user?.employeeId || uuid(); // Use the employeeId if available, otherwise generate a UUID + + // Construct and return the subdirectory path + return path.join(tenantId, employeeId); + } +} diff --git a/packages/core/src/lib/core/file-storage/helpers/file-storage-factory.ts b/packages/core/src/lib/core/file-storage/helpers/file-storage-factory.ts new file mode 100644 index 00000000000..adf0a19c5b5 --- /dev/null +++ b/packages/core/src/lib/core/file-storage/helpers/file-storage-factory.ts @@ -0,0 +1,20 @@ +import multer from 'multer'; +import * as path from 'path'; +import { FileStorage } from '../file-storage'; +import { DirectoryPathGenerator } from './directory-path-generator'; +import { IDirectoryPathGenerator } from './directory-path-generator.interface'; + +// FileStorageFactory +export class FileStorageFactory { + private static readonly pathGenerator: IDirectoryPathGenerator = new DirectoryPathGenerator(); + + public static create(baseDirname: string): multer.StorageEngine { + const baseDirectory = this.pathGenerator.getBaseDirectory(baseDirname); + const subDirectory = this.pathGenerator.getSubDirectory(); + + return new FileStorage().storage({ + dest: () => path.join(baseDirectory, subDirectory), + prefix: baseDirname + }); + } +} diff --git a/packages/core/src/lib/core/file-storage/helpers/index.ts b/packages/core/src/lib/core/file-storage/helpers/index.ts new file mode 100644 index 00000000000..72d377ccdbf --- /dev/null +++ b/packages/core/src/lib/core/file-storage/helpers/index.ts @@ -0,0 +1,3 @@ +export * from './directory-path-generator'; +export * from './directory-path-generator.interface'; +export * from './file-storage-factory'; diff --git a/packages/core/src/lib/core/file-storage/index.ts b/packages/core/src/lib/core/file-storage/index.ts index 1bb71c141f5..12559986cec 100644 --- a/packages/core/src/lib/core/file-storage/index.ts +++ b/packages/core/src/lib/core/file-storage/index.ts @@ -1,4 +1,5 @@ export * from './file-storage'; export * from './file-storage.module'; +export * from './helpers'; export * from './tenant-settings.middleware'; export * from './uploaded-file-storage'; diff --git a/packages/core/src/lib/core/interceptors/index.ts b/packages/core/src/lib/core/interceptors/index.ts index b40de9b64a6..6d8a1de0332 100644 --- a/packages/core/src/lib/core/interceptors/index.ts +++ b/packages/core/src/lib/core/interceptors/index.ts @@ -1,4 +1,4 @@ export * from './cloud-migrate.interceptor'; export * from './serializer.interceptor'; export * from './lazy-file-interceptor'; -export * from './transform.interceptor'; \ No newline at end of file +export * from './transform.interceptor'; diff --git a/packages/core/src/lib/database/migrations/1735058989058-CreateVideoTable.ts b/packages/core/src/lib/database/migrations/1735058989058-CreateVideoTable.ts new file mode 100644 index 00000000000..04b7c9f1008 --- /dev/null +++ b/packages/core/src/lib/database/migrations/1735058989058-CreateVideoTable.ts @@ -0,0 +1,207 @@ + +import { MigrationInterface, QueryRunner } from "typeorm"; +import * as chalk from 'chalk'; +import { DatabaseTypeEnum } from "@gauzy/config"; + +export class CreateVideoTable1735058989058 implements MigrationInterface { + + name = 'CreateVideoTable1735058989058'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(chalk.yellow(this.name + ' start running!')); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."video_storageprovider_enum" AS ENUM('LOCAL', 'S3', 'WASABI', 'CLOUDINARY', 'DIGITALOCEAN')`); + await queryRunner.query(`CREATE TABLE "video" ("deletedAt" TIMESTAMP, "id" uuid NOT NULL DEFAULT gen_random_uuid(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean DEFAULT true, "isArchived" boolean DEFAULT false, "archivedAt" TIMESTAMP, "tenantId" uuid, "organizationId" uuid, "title" character varying NOT NULL, "file" character varying NOT NULL, "recordedAt" TIMESTAMP, "duration" integer, "size" integer, "fullUrl" character varying, "storageProvider" "public"."video_storageprovider_enum", "description" character varying, "resolution" character varying DEFAULT '1920:1080', "codec" character varying DEFAULT 'libx264', "frameRate" integer DEFAULT '15', "timeSlotId" uuid, "uploadedById" uuid, CONSTRAINT "PK_e4c86c0cf95aff16e9fb8220f6b" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_eea665c6f09c4cd9a520a028d1" ON "video" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_4adb6b1409e7b614d06e44fb84" ON "video" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_1f9ad46e9fbeddbc609af9976a" ON "video" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_dcbf77e688d65ced41055c3faf" ON "video" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb34a3e97002e3af9cc219f4e4" ON "video" ("recordedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_b3e784ea168736c83f4d647abf" ON "video" ("storageProvider") `); + await queryRunner.query(`CREATE INDEX "IDX_d5a38b4293d90e31a6b1f3189e" ON "video" ("timeSlotId") `); + await queryRunner.query(`CREATE INDEX "IDX_159f8e5c7959016a0863ec419a" ON "video" ("uploadedById") `); + await queryRunner.query(`ALTER TABLE "video" ADD CONSTRAINT "FK_1f9ad46e9fbeddbc609af9976ae" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "video" ADD CONSTRAINT "FK_dcbf77e688d65ced41055c3fafe" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "video" ADD CONSTRAINT "FK_d5a38b4293d90e31a6b1f3189e1" FOREIGN KEY ("timeSlotId") REFERENCES "time_slot"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "video" ADD CONSTRAINT "FK_159f8e5c7959016a0863ec419a3" FOREIGN KEY ("uploadedById") REFERENCES "employee"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "video" DROP CONSTRAINT "FK_159f8e5c7959016a0863ec419a3"`); + await queryRunner.query(`ALTER TABLE "video" DROP CONSTRAINT "FK_d5a38b4293d90e31a6b1f3189e1"`); + await queryRunner.query(`ALTER TABLE "video" DROP CONSTRAINT "FK_dcbf77e688d65ced41055c3fafe"`); + await queryRunner.query(`ALTER TABLE "video" DROP CONSTRAINT "FK_1f9ad46e9fbeddbc609af9976ae"`); + await queryRunner.query(`DROP INDEX "public"."IDX_159f8e5c7959016a0863ec419a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d5a38b4293d90e31a6b1f3189e"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b3e784ea168736c83f4d647abf"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cb34a3e97002e3af9cc219f4e4"`); + await queryRunner.query(`DROP INDEX "public"."IDX_dcbf77e688d65ced41055c3faf"`); + await queryRunner.query(`DROP INDEX "public"."IDX_1f9ad46e9fbeddbc609af9976a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4adb6b1409e7b614d06e44fb84"`); + await queryRunner.query(`DROP INDEX "public"."IDX_eea665c6f09c4cd9a520a028d1"`); + await queryRunner.query(`DROP TABLE "video"`); + await queryRunner.query(`DROP TYPE "public"."video_storageprovider_enum"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "video" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "file" varchar NOT NULL, "recordedAt" datetime, "duration" integer, "size" integer, "fullUrl" varchar, "storageProvider" varchar CHECK( "storageProvider" IN ('LOCAL','S3','WASABI','CLOUDINARY','DIGITALOCEAN') ), "description" varchar, "resolution" varchar DEFAULT ('1920:1080'), "codec" varchar DEFAULT ('libx264'), "frameRate" integer DEFAULT (15), "timeSlotId" varchar, "uploadedById" varchar)`); + await queryRunner.query(`CREATE INDEX "IDX_eea665c6f09c4cd9a520a028d1" ON "video" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_4adb6b1409e7b614d06e44fb84" ON "video" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_1f9ad46e9fbeddbc609af9976a" ON "video" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_dcbf77e688d65ced41055c3faf" ON "video" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb34a3e97002e3af9cc219f4e4" ON "video" ("recordedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_b3e784ea168736c83f4d647abf" ON "video" ("storageProvider") `); + await queryRunner.query(`CREATE INDEX "IDX_d5a38b4293d90e31a6b1f3189e" ON "video" ("timeSlotId") `); + await queryRunner.query(`CREATE INDEX "IDX_159f8e5c7959016a0863ec419a" ON "video" ("uploadedById") `); + await queryRunner.query(`DROP INDEX "IDX_eea665c6f09c4cd9a520a028d1"`); + await queryRunner.query(`DROP INDEX "IDX_4adb6b1409e7b614d06e44fb84"`); + await queryRunner.query(`DROP INDEX "IDX_1f9ad46e9fbeddbc609af9976a"`); + await queryRunner.query(`DROP INDEX "IDX_dcbf77e688d65ced41055c3faf"`); + await queryRunner.query(`DROP INDEX "IDX_cb34a3e97002e3af9cc219f4e4"`); + await queryRunner.query(`DROP INDEX "IDX_b3e784ea168736c83f4d647abf"`); + await queryRunner.query(`DROP INDEX "IDX_d5a38b4293d90e31a6b1f3189e"`); + await queryRunner.query(`DROP INDEX "IDX_159f8e5c7959016a0863ec419a"`); + await queryRunner.query(`CREATE TABLE "temporary_video" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "file" varchar NOT NULL, "recordedAt" datetime, "duration" integer, "size" integer, "fullUrl" varchar, "storageProvider" varchar CHECK( "storageProvider" IN ('LOCAL','S3','WASABI','CLOUDINARY','DIGITALOCEAN') ), "description" varchar, "resolution" varchar DEFAULT ('1920:1080'), "codec" varchar DEFAULT ('libx264'), "frameRate" integer DEFAULT (15), "timeSlotId" varchar, "uploadedById" varchar, CONSTRAINT "FK_1f9ad46e9fbeddbc609af9976ae" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_dcbf77e688d65ced41055c3fafe" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_d5a38b4293d90e31a6b1f3189e1" FOREIGN KEY ("timeSlotId") REFERENCES "time_slot" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_159f8e5c7959016a0863ec419a3" FOREIGN KEY ("uploadedById") REFERENCES "employee" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_video"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "title", "file", "recordedAt", "duration", "size", "fullUrl", "storageProvider", "description", "resolution", "codec", "frameRate", "timeSlotId", "uploadedById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "title", "file", "recordedAt", "duration", "size", "fullUrl", "storageProvider", "description", "resolution", "codec", "frameRate", "timeSlotId", "uploadedById" FROM "video"`); + await queryRunner.query(`DROP TABLE "video"`); + await queryRunner.query(`ALTER TABLE "temporary_video" RENAME TO "video"`); + await queryRunner.query(`CREATE INDEX "IDX_eea665c6f09c4cd9a520a028d1" ON "video" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_4adb6b1409e7b614d06e44fb84" ON "video" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_1f9ad46e9fbeddbc609af9976a" ON "video" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_dcbf77e688d65ced41055c3faf" ON "video" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb34a3e97002e3af9cc219f4e4" ON "video" ("recordedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_b3e784ea168736c83f4d647abf" ON "video" ("storageProvider") `); + await queryRunner.query(`CREATE INDEX "IDX_d5a38b4293d90e31a6b1f3189e" ON "video" ("timeSlotId") `); + await queryRunner.query(`CREATE INDEX "IDX_159f8e5c7959016a0863ec419a" ON "video" ("uploadedById") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_159f8e5c7959016a0863ec419a"`); + await queryRunner.query(`DROP INDEX "IDX_d5a38b4293d90e31a6b1f3189e"`); + await queryRunner.query(`DROP INDEX "IDX_b3e784ea168736c83f4d647abf"`); + await queryRunner.query(`DROP INDEX "IDX_cb34a3e97002e3af9cc219f4e4"`); + await queryRunner.query(`DROP INDEX "IDX_dcbf77e688d65ced41055c3faf"`); + await queryRunner.query(`DROP INDEX "IDX_1f9ad46e9fbeddbc609af9976a"`); + await queryRunner.query(`DROP INDEX "IDX_4adb6b1409e7b614d06e44fb84"`); + await queryRunner.query(`DROP INDEX "IDX_eea665c6f09c4cd9a520a028d1"`); + await queryRunner.query(`ALTER TABLE "video" RENAME TO "temporary_video"`); + await queryRunner.query(`CREATE TABLE "video" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "file" varchar NOT NULL, "recordedAt" datetime, "duration" integer, "size" integer, "fullUrl" varchar, "storageProvider" varchar CHECK( "storageProvider" IN ('LOCAL','S3','WASABI','CLOUDINARY','DIGITALOCEAN') ), "description" varchar, "resolution" varchar DEFAULT ('1920:1080'), "codec" varchar DEFAULT ('libx264'), "frameRate" integer DEFAULT (15), "timeSlotId" varchar, "uploadedById" varchar)`); + await queryRunner.query(`INSERT INTO "video"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "title", "file", "recordedAt", "duration", "size", "fullUrl", "storageProvider", "description", "resolution", "codec", "frameRate", "timeSlotId", "uploadedById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "title", "file", "recordedAt", "duration", "size", "fullUrl", "storageProvider", "description", "resolution", "codec", "frameRate", "timeSlotId", "uploadedById" FROM "temporary_video"`); + await queryRunner.query(`DROP TABLE "temporary_video"`); + await queryRunner.query(`CREATE INDEX "IDX_159f8e5c7959016a0863ec419a" ON "video" ("uploadedById") `); + await queryRunner.query(`CREATE INDEX "IDX_d5a38b4293d90e31a6b1f3189e" ON "video" ("timeSlotId") `); + await queryRunner.query(`CREATE INDEX "IDX_b3e784ea168736c83f4d647abf" ON "video" ("storageProvider") `); + await queryRunner.query(`CREATE INDEX "IDX_cb34a3e97002e3af9cc219f4e4" ON "video" ("recordedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_dcbf77e688d65ced41055c3faf" ON "video" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_1f9ad46e9fbeddbc609af9976a" ON "video" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_4adb6b1409e7b614d06e44fb84" ON "video" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_eea665c6f09c4cd9a520a028d1" ON "video" ("isActive") `); + await queryRunner.query(`DROP INDEX "IDX_159f8e5c7959016a0863ec419a"`); + await queryRunner.query(`DROP INDEX "IDX_d5a38b4293d90e31a6b1f3189e"`); + await queryRunner.query(`DROP INDEX "IDX_b3e784ea168736c83f4d647abf"`); + await queryRunner.query(`DROP INDEX "IDX_cb34a3e97002e3af9cc219f4e4"`); + await queryRunner.query(`DROP INDEX "IDX_dcbf77e688d65ced41055c3faf"`); + await queryRunner.query(`DROP INDEX "IDX_1f9ad46e9fbeddbc609af9976a"`); + await queryRunner.query(`DROP INDEX "IDX_4adb6b1409e7b614d06e44fb84"`); + await queryRunner.query(`DROP INDEX "IDX_eea665c6f09c4cd9a520a028d1"`); + await queryRunner.query(`DROP TABLE "video"`); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE \`video\` (\`deletedAt\` datetime(6) NULL, \`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`isActive\` tinyint NULL DEFAULT 1, \`isArchived\` tinyint NULL DEFAULT 0, \`archivedAt\` datetime NULL, \`tenantId\` varchar(255) NULL, \`organizationId\` varchar(255) NULL, \`title\` varchar(255) NOT NULL, \`file\` varchar(255) NOT NULL, \`recordedAt\` datetime NULL, \`duration\` int NULL, \`size\` int NULL, \`fullUrl\` varchar(255) NULL, \`storageProvider\` enum ('LOCAL', 'S3', 'WASABI', 'CLOUDINARY', 'DIGITALOCEAN') NULL, \`description\` varchar(255) NULL, \`resolution\` varchar(255) NULL DEFAULT '1920:1080', \`codec\` varchar(255) NULL DEFAULT 'libx264', \`frameRate\` int NULL DEFAULT '15', \`timeSlotId\` varchar(255) NULL, \`uploadedById\` varchar(255) NULL, INDEX \`IDX_eea665c6f09c4cd9a520a028d1\` (\`isActive\`), INDEX \`IDX_4adb6b1409e7b614d06e44fb84\` (\`isArchived\`), INDEX \`IDX_1f9ad46e9fbeddbc609af9976a\` (\`tenantId\`), INDEX \`IDX_dcbf77e688d65ced41055c3faf\` (\`organizationId\`), INDEX \`IDX_cb34a3e97002e3af9cc219f4e4\` (\`recordedAt\`), INDEX \`IDX_b3e784ea168736c83f4d647abf\` (\`storageProvider\`), INDEX \`IDX_d5a38b4293d90e31a6b1f3189e\` (\`timeSlotId\`), INDEX \`IDX_159f8e5c7959016a0863ec419a\` (\`uploadedById\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`ALTER TABLE \`video\` ADD CONSTRAINT \`FK_1f9ad46e9fbeddbc609af9976ae\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE \`video\` ADD CONSTRAINT \`FK_dcbf77e688d65ced41055c3fafe\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE \`video\` ADD CONSTRAINT \`FK_d5a38b4293d90e31a6b1f3189e1\` FOREIGN KEY (\`timeSlotId\`) REFERENCES \`time_slot\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE \`video\` ADD CONSTRAINT \`FK_159f8e5c7959016a0863ec419a3\` FOREIGN KEY (\`uploadedById\`) REFERENCES \`employee\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`video\` DROP FOREIGN KEY \`FK_159f8e5c7959016a0863ec419a3\``); + await queryRunner.query(`ALTER TABLE \`video\` DROP FOREIGN KEY \`FK_d5a38b4293d90e31a6b1f3189e1\``); + await queryRunner.query(`ALTER TABLE \`video\` DROP FOREIGN KEY \`FK_dcbf77e688d65ced41055c3fafe\``); + await queryRunner.query(`ALTER TABLE \`video\` DROP FOREIGN KEY \`FK_1f9ad46e9fbeddbc609af9976ae\``); + await queryRunner.query(`DROP INDEX \`IDX_159f8e5c7959016a0863ec419a\` ON \`video\``); + await queryRunner.query(`DROP INDEX \`IDX_d5a38b4293d90e31a6b1f3189e\` ON \`video\``); + await queryRunner.query(`DROP INDEX \`IDX_b3e784ea168736c83f4d647abf\` ON \`video\``); + await queryRunner.query(`DROP INDEX \`IDX_cb34a3e97002e3af9cc219f4e4\` ON \`video\``); + await queryRunner.query(`DROP INDEX \`IDX_dcbf77e688d65ced41055c3faf\` ON \`video\``); + await queryRunner.query(`DROP INDEX \`IDX_1f9ad46e9fbeddbc609af9976a\` ON \`video\``); + await queryRunner.query(`DROP INDEX \`IDX_4adb6b1409e7b614d06e44fb84\` ON \`video\``); + await queryRunner.query(`DROP INDEX \`IDX_eea665c6f09c4cd9a520a028d1\` ON \`video\``); + await queryRunner.query(`DROP TABLE \`video\``); + } +} diff --git a/packages/plugins/video-capture/.dockerignore b/packages/plugins/video-capture/.dockerignore new file mode 100644 index 00000000000..14ea7e1f315 --- /dev/null +++ b/packages/plugins/video-capture/.dockerignore @@ -0,0 +1,23 @@ +# Ignore Git related files and directories +.git +.gitignore +.gitmodules + +# Ignore README file +README.md + +# Ignore Docker-related files and directories +docker + +# Ignore Node.js modules +node_modules + +# Ignore temporary files and directories +tmp + +# Ignore build and compilation output +build +dist + +# Ignore environment configuration files +.env diff --git a/packages/plugins/video-capture/.gitignore b/packages/plugins/video-capture/.gitignore new file mode 100644 index 00000000000..84e698df22c --- /dev/null +++ b/packages/plugins/video-capture/.gitignore @@ -0,0 +1,11 @@ + +# dependencies +node_modules/ + +# yarn +yarn-error.log + +# misc +npm-debug.log +dist +build diff --git a/packages/plugins/video-capture/.npmignore b/packages/plugins/video-capture/.npmignore new file mode 100644 index 00000000000..1eb4beb9572 --- /dev/null +++ b/packages/plugins/video-capture/.npmignore @@ -0,0 +1,4 @@ +# .npmignore + +src/ +node_modules/ diff --git a/packages/plugins/video-capture/CHANGELOG.md b/packages/plugins/video-capture/CHANGELOG.md new file mode 100644 index 00000000000..364dc5114a3 --- /dev/null +++ b/packages/plugins/video-capture/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog for @gauzy/plugin-video-capture + +## [Unreleased] diff --git a/packages/plugins/video-capture/README.md b/packages/plugins/video-capture/README.md new file mode 100644 index 00000000000..7bda8daa79d --- /dev/null +++ b/packages/plugins/video-capture/README.md @@ -0,0 +1,29 @@ +# @gauzy/plugin-video-capture + +This library was generated with [Nx](https://nx.dev). It contains the Video Capture plugin for the Gauzy platform. + +## Overview + +This plugin provides continuous video recording capabilities for your projects. + +## Building + +Run `yarn nx build plugin-video-capture` to build the library. + +## Running unit tests + +Run `yarn nx test plugin-video-capture` to execute the unit tests via [Jest](https://jestjs.io). + +## Publishing + +After building your library with `yarn nx build plugin-video-capture`, go to the dist folder `dist/packages/plugins/video-capture` and run `npm publish`. + +## Installation + +Install the Video Capture plugin using your preferred package manager: + +```bash +npm install @gauzy/plugin-video-capture +# or +yarn add @gauzy/plugin-video-capture +``` diff --git a/packages/plugins/video-capture/eslint.config.js b/packages/plugins/video-capture/eslint.config.js new file mode 100644 index 00000000000..ba4f5d8daad --- /dev/null +++ b/packages/plugins/video-capture/eslint.config.js @@ -0,0 +1,19 @@ +const baseConfig = require('../../../.eslintrc.json'); + +module.exports = [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'error', + { + ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'] + } + ] + }, + languageOptions: { + parser: require('jsonc-eslint-parser') + } + } +]; diff --git a/packages/plugins/video-capture/jest.config.ts b/packages/plugins/video-capture/jest.config.ts new file mode 100644 index 00000000000..3911db51777 --- /dev/null +++ b/packages/plugins/video-capture/jest.config.ts @@ -0,0 +1,10 @@ +export default { + displayName: 'plugin-video-capture', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }] + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/packages/plugins/video-capture' +}; diff --git a/packages/plugins/video-capture/package.json b/packages/plugins/video-capture/package.json new file mode 100644 index 00000000000..134549a1b8d --- /dev/null +++ b/packages/plugins/video-capture/package.json @@ -0,0 +1,59 @@ +{ + "name": "@gauzy/plugin-video-capture", + "version": "0.1.0", + "description": "Ever Gauzy Platform plugin for continuous video recording", + "author": { + "name": "Ever Co. LTD", + "email": "ever@ever.co", + "url": "https://ever.co" + }, + "repository": { + "type": "git", + "url": "https://github.com/ever-co/ever-gauzy" + }, + "bugs": { + "url": "https://github.com/ever-co/ever-gauzy/issues" + }, + "homepage": "https://ever.co", + "license": "AGPL-3.0", + "private": true, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts", + "scripts": { + "lib:build": "yarn nx build plugin-video-capture", + "lib:build:prod": "yarn nx build plugin-video-capture", + "lib:watch": "yarn nx build plugin-video-capture --watch" + }, + "dependencies": { + "@gauzy/contracts": "^0.1.0", + "@gauzy/core": "^0.1.0", + "@gauzy/plugin": "^0.1.0", + "tslib": "^2.6.2" + }, + "devDependencies": { + "@types/express": "^4.17.13", + "@types/jest": "29.5.14", + "@types/node": "^20.14.9", + "typescript": "5.5.4" + }, + "keywords": [ + "video", + "capture", + "plugin", + "NestJS", + "Gauzy", + "Ever Gauzy", + "platform", + "management", + "tool", + "software", + "development", + "typescript" + ], + "engines": { + "node": ">=20.11.1", + "yarn": ">=1.22.19" + }, + "sideEffects": false +} diff --git a/packages/plugins/video-capture/project.json b/packages/plugins/video-capture/project.json new file mode 100644 index 00000000000..284de0f241b --- /dev/null +++ b/packages/plugins/video-capture/project.json @@ -0,0 +1,44 @@ +{ + "name": "plugin-video-capture", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/plugins/video-capture/src", + "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist/{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], + "implicitDependencies": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/plugins/video-capture", + "tsConfig": "packages/plugins/video-capture/tsconfig.lib.json", + "packageJson": "packages/plugins/video-capture/package.json", + "main": "packages/plugins/video-capture/src/index.ts", + "assets": ["packages/plugins/video-capture/*.md"] + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/plugins/video-capture/jest.config.ts" + } + } + } +} diff --git a/packages/plugins/video-capture/src/index.ts b/packages/plugins/video-capture/src/index.ts new file mode 100644 index 00000000000..eff3a286876 --- /dev/null +++ b/packages/plugins/video-capture/src/index.ts @@ -0,0 +1,5 @@ +// Export the main plugin module +export * from './lib/video-capture.plugin'; + +// Export individual models +export * from './lib/video.model'; diff --git a/packages/plugins/video-capture/src/lib/commands/create-video.command.ts b/packages/plugins/video-capture/src/lib/commands/create-video.command.ts new file mode 100644 index 00000000000..cc322194753 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/commands/create-video.command.ts @@ -0,0 +1,7 @@ +import { ICommand } from '@nestjs/cqrs'; +import { CreateVideoDTO } from '../dto'; + +export class CreateVideoCommand implements ICommand { + public static readonly type = '[Create] Video'; + constructor(public readonly input: CreateVideoDTO) {} +} diff --git a/packages/plugins/video-capture/src/lib/commands/delete-video.command.ts b/packages/plugins/video-capture/src/lib/commands/delete-video.command.ts new file mode 100644 index 00000000000..ec635f9d610 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/commands/delete-video.command.ts @@ -0,0 +1,7 @@ +import { ICommand } from '@nestjs/cqrs'; +import { DeleteVideoDTO } from '../dto'; + +export class DeleteVideoCommand implements ICommand { + public static readonly type = '[Delete] Video'; + constructor(public readonly input: DeleteVideoDTO) {} +} diff --git a/packages/plugins/video-capture/src/lib/commands/handlers/create-video.handler.ts b/packages/plugins/video-capture/src/lib/commands/handlers/create-video.handler.ts new file mode 100644 index 00000000000..b87e0f712b7 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/commands/handlers/create-video.handler.ts @@ -0,0 +1,31 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { IVideo } from '../../video.model'; +import { Video } from '../../entities/video.entity'; +import { VideosService } from '../../services/videos.service'; +import { CreateVideoCommand } from '../create-video.command'; + +@CommandHandler(CreateVideoCommand) +export class CreateVideoHandler implements ICommandHandler { + constructor(private readonly videosService: VideosService) {} + + /** + * Handles the `CreateVideoCommand` to create a new video entity in the database. + * + * @param command - The `CreateVideoCommand` containing the input data for the new video. + * + * @returns A promise resolving to the newly created video entity (`IVideo`). + */ + public async execute(command: CreateVideoCommand): Promise { + // Extract input data from the command + const { input } = command; + + // Step 1: Create a new video entity with the provided input + const video = new Video({ + ...input, + file: input.file.key // Extract the file key if a file object is provided + }); + + // Step 2: Save the new video entity to the database + return this.videosService.create(video); + } +} diff --git a/packages/plugins/video-capture/src/lib/commands/handlers/delete-video.handler.ts b/packages/plugins/video-capture/src/lib/commands/handlers/delete-video.handler.ts new file mode 100644 index 00000000000..8dfd976525b --- /dev/null +++ b/packages/plugins/video-capture/src/lib/commands/handlers/delete-video.handler.ts @@ -0,0 +1,40 @@ +import { NotFoundException } from '@nestjs/common'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { DeleteResult } from 'typeorm'; +import { VideosService } from '../../services/videos.service'; +import { DeleteVideoCommand } from '../delete-video.command'; + +@CommandHandler(DeleteVideoCommand) +export class DeleteVideoHandler implements ICommandHandler { + constructor(private readonly videosService: VideosService) {} + + /** + * Handles the `DeleteVideoCommand` to delete a video entity from the database. + * Validates the existence of the video and performs the deletion based on the provided criteria. + * + * @param command - The `DeleteVideoCommand` containing the video ID and additional options for deletion. + * + * @returns A promise resolving to a `DeleteResult`, which includes metadata about the deletion operation. + * + * @throws {NotFoundException} If the video with the specified ID does not exist. + */ + public async execute(command: DeleteVideoCommand): Promise { + // Destructure the command to extract input data + const { + input: { id, options = {} } + } = command; + + // Step 1: Check if the video exists + const video = await this.videosService.findOneByWhereOptions({ ...options, id }); + + // Step 2: Throw a NotFoundException if the video does not exist + if (!video) { + throw new NotFoundException(`Video with ID ${id} not found.`); + } + + // Step 3: Delete the video entity from the database + return this.videosService.delete(id, { + where: options + }); + } +} diff --git a/packages/plugins/video-capture/src/lib/commands/handlers/index.ts b/packages/plugins/video-capture/src/lib/commands/handlers/index.ts new file mode 100644 index 00000000000..93f9834b6ad --- /dev/null +++ b/packages/plugins/video-capture/src/lib/commands/handlers/index.ts @@ -0,0 +1,5 @@ +import { CreateVideoHandler } from './create-video.handler'; +import { DeleteVideoHandler } from './delete-video.handler'; +import { UpdateVideoHandler } from './update-video.handler'; + +export const CommandHandlers = [CreateVideoHandler, DeleteVideoHandler, UpdateVideoHandler]; diff --git a/packages/plugins/video-capture/src/lib/commands/handlers/update-video.handler.ts b/packages/plugins/video-capture/src/lib/commands/handlers/update-video.handler.ts new file mode 100644 index 00000000000..1ab3fe25f28 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/commands/handlers/update-video.handler.ts @@ -0,0 +1,37 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { IVideo } from '../../video.model'; +import { VideosService } from '../../services/videos.service'; +import { UpdateVideoCommand } from '../update-video.command'; + +@CommandHandler(UpdateVideoCommand) +export class UpdateVideoHandler implements ICommandHandler { + constructor(public readonly videosService: VideosService) {} + + /** + * Handles the update of a video entity in the database. + * This method receives an `UpdateVideoCommand`, updates the video entity with the provided data, + * and returns the updated video entity. + * + * @param command - The command containing the input data for updating the video. + * + * @returns A promise that resolves to the updated video entity (`IVideo`). + */ + public async execute(command: UpdateVideoCommand): Promise { + // Extract input data from the command + const { input } = command; + + // Destructure the input fields for clarity + const { id, title, size, file, duration } = input; + + // Update the video entity in the database using the provided ID and input fields + await this.videosService.update(id, { + title, + size, + duration, + file: file?.key // Extract the file key if a file object is provided + }); + + // Fetch and return the updated video entity by its ID + return this.videosService.findOneByIdString(id); + } +} diff --git a/packages/plugins/video-capture/src/lib/commands/update-video.command.ts b/packages/plugins/video-capture/src/lib/commands/update-video.command.ts new file mode 100644 index 00000000000..43c0cd3888e --- /dev/null +++ b/packages/plugins/video-capture/src/lib/commands/update-video.command.ts @@ -0,0 +1,7 @@ +import { ICommand } from '@nestjs/cqrs'; +import { UpdateVideoDTO } from '../dto'; + +export class UpdateVideoCommand implements ICommand { + public static readonly type = '[Update] Video'; + constructor(public readonly input: UpdateVideoDTO) {} +} diff --git a/packages/plugins/video-capture/src/lib/dto/base-video.dto.ts b/packages/plugins/video-capture/src/lib/dto/base-video.dto.ts new file mode 100644 index 00000000000..7121370a27f --- /dev/null +++ b/packages/plugins/video-capture/src/lib/dto/base-video.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { Video } from '../entities/video.entity'; + +export class BaseVideoDTO extends OmitType(Video, ['id', 'file', 'createdAt', 'updatedAt', 'deletedAt'] as const) {} diff --git a/packages/plugins/video-capture/src/lib/dto/create-video.dto.ts b/packages/plugins/video-capture/src/lib/dto/create-video.dto.ts new file mode 100644 index 00000000000..7b2c9da74c0 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/dto/create-video.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsDefined, IsNotEmpty, Length, Matches, ValidateNested } from 'class-validator'; +import { BaseVideoDTO } from './base-video.dto'; +import { FileDTO } from './file.dto'; + +export class CreateVideoDTO extends BaseVideoDTO { + @ApiProperty({ + description: 'The title of the video', + example: 'My Project Demo Video 2024', + minLength: 3, + maxLength: 255 + }) + @IsDefined({ message: 'Title is required and cannot be empty.' }) + @IsNotEmpty({ message: 'Title is required' }) + @Length(3, 255, { message: 'Title must be between 3 and 255 characters' }) + @Matches(/^[\w\s-]+$/i, { + message: 'Title can only contain letters, numbers, spaces, and hyphens' + }) + title: string; + + @ApiProperty({ + type: FileDTO, + description: 'The uploaded video file object containing metadata and properties', + required: true + }) + @ValidateNested({ message: 'File must be a valid FileDTO object' }) + @Type(() => FileDTO) + file: FileDTO; +} diff --git a/packages/plugins/video-capture/src/lib/dto/delete-video.dto.ts b/packages/plugins/video-capture/src/lib/dto/delete-video.dto.ts new file mode 100644 index 00000000000..c9cc9448422 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/dto/delete-video.dto.ts @@ -0,0 +1,21 @@ +import { ID } from '@gauzy/contracts'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { IDeleteVideo } from '../video.model'; + +export class DeleteVideoDTO { + /** + * The ID of the video to delete + */ + @ApiProperty({ description: 'The ID of the video to delete', example: '123e4567-e89b-12d3-a456-426614174000' }) + @IsNotEmpty() + @IsUUID() + readonly id: ID; + + /** + * The options to delete the video + */ + @ApiProperty({ required: false }) + @IsOptional() + options?: IDeleteVideo; +} diff --git a/packages/plugins/video-capture/src/lib/dto/file.dto.ts b/packages/plugins/video-capture/src/lib/dto/file.dto.ts new file mode 100644 index 00000000000..5d302c4273a --- /dev/null +++ b/packages/plugins/video-capture/src/lib/dto/file.dto.ts @@ -0,0 +1,94 @@ +import { UploadedFile } from '@gauzy/contracts'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsOptional, IsPositive, IsString, Matches, MaxLength, Min } from 'class-validator'; + +export class FileDTO implements UploadedFile { + @ApiProperty({ + description: 'The field name associated with the file', + example: 'videoFile' + }) + @IsNotEmpty({ message: 'Field name must not be empty' }) + @IsString({ message: 'Field name must be a string' }) + @MaxLength(255, { message: 'Field name must not exceed 255 characters' }) + fieldname: string; + + @ApiProperty({ + description: 'The file path or identifier of the video', + example: 'project-demo-2024.mp4', + pattern: '/\\.(mp4)$/' + }) + @IsNotEmpty({ message: 'File key must not be empty' }) + @IsString({ message: 'File key must be a string' }) + @MaxLength(255, { message: 'File key must not exceed 255 characters' }) + @Matches(/\.(mp4)$/, { + message: + 'File must be a valid video format mp4 and contain only letters, numbers, spaces, hyphens, or underscores' + }) + key: string; + + @ApiProperty({ + description: 'The original file name', + example: 'project-demo-original.mp4' + }) + @IsNotEmpty({ message: 'Original name must not be empty' }) + @IsString({ message: 'Original name must be a string' }) + @MaxLength(255, { message: 'Original name must not exceed 255 characters' }) + originalname: string; + + @ApiProperty({ + description: 'The size of the file in bytes', + example: 10485760 + }) + @IsNotEmpty({ message: 'File size must not be empty' }) + @IsNumber({}, { message: 'File size must be a number' }) + @IsPositive({ message: 'File size must be a positive number' }) + @Min(1, { message: 'File size must be at least 1 byte' }) + size: number; + + @ApiProperty({ + description: 'The file encoding (if available)', + example: '7bit' + }) + @IsOptional() + @IsString({ message: 'Encoding must be a string' }) + @MaxLength(50, { message: 'Encoding must not exceed 50 characters' }) + encoding?: string; + + @ApiProperty({ + description: 'The MIME type of the file (if available)', + example: 'video/mp4' + }) + @IsOptional() + @IsString({ message: 'MIME type must be a string' }) + @MaxLength(50, { message: 'MIME type must not exceed 50 characters' }) + @Matches(/^video\/(mp4)$/, { + message: 'MIME type must be a valid video MIME type video/mp4' + }) + mimetype?: string; + + @ApiProperty({ + description: 'The file name', + example: 'project-demo-2024.mp4' + }) + @IsNotEmpty({ message: 'File name must not be empty' }) + @IsString({ message: 'File name must be a string' }) + @MaxLength(255, { message: 'File name must not exceed 255 characters' }) + filename: string; + + @ApiProperty({ + description: 'The public URL of the file', + example: 'https://example.com/project-demo-2024.mp4' + }) + @IsNotEmpty({ message: 'File URL must not be empty' }) + @MaxLength(2083, { message: 'File URL must not exceed 2083 characters' }) // 2083 is the maximum URL length in browsers + url: string; + + @ApiProperty({ + description: 'The full path of the file', + example: '/uploads/project-demo-2024.mp4' + }) + @IsNotEmpty({ message: 'File path must not be empty' }) + @IsString({ message: 'File path must be a string' }) + @MaxLength(1024, { message: 'File path must not exceed 1024 characters' }) + path: string; +} diff --git a/packages/plugins/video-capture/src/lib/dto/index.ts b/packages/plugins/video-capture/src/lib/dto/index.ts new file mode 100644 index 00000000000..08f52ea1623 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/dto/index.ts @@ -0,0 +1,5 @@ +export * from './base-video.dto'; +export * from './create-video.dto'; +export * from './delete-video.dto'; +export * from './file.dto'; +export * from './update-video.dto'; diff --git a/packages/plugins/video-capture/src/lib/dto/update-video.dto.ts b/packages/plugins/video-capture/src/lib/dto/update-video.dto.ts new file mode 100644 index 00000000000..f30f52070a9 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/dto/update-video.dto.ts @@ -0,0 +1,19 @@ +import { ID } from '@gauzy/contracts'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsOptional, IsUUID, ValidateNested } from 'class-validator'; +import { BaseVideoDTO } from './base-video.dto'; +import { FileDTO } from './file.dto'; + +export class UpdateVideoDTO extends BaseVideoDTO { + @ApiProperty({ description: 'The ID of the video to update', example: '123e4567-e89b-12d3-a456-426614174000' }) + @IsNotEmpty() + @IsUUID() + id: ID; + + @ApiProperty({ type: FileDTO }) + @ValidateNested() + @Type(() => FileDTO) + @IsOptional() + file?: FileDTO; +} diff --git a/packages/plugins/video-capture/src/lib/entities/video.entity.ts b/packages/plugins/video-capture/src/lib/entities/video.entity.ts new file mode 100644 index 00000000000..f89be25e021 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/entities/video.entity.ts @@ -0,0 +1,229 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Exclude, Transform } from 'class-transformer'; +import { + IsDateString, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUrl, + IsUUID, + Length, + Matches, + Max, + Min, + ValidateIf +} from 'class-validator'; +import { JoinColumn, RelationId } from 'typeorm'; +import { FileStorageProvider, FileStorageProviderEnum, ID, IEmployee, ITimeSlot } from '@gauzy/contracts'; +import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from '@gauzy/core'; +import { Employee, TenantOrganizationBaseEntity, TimeSlot } from '@gauzy/core'; +import { IVideo, VideoCodecEnum, VideoResolutionEnum } from '../video.model'; +import { MikroOrmVideoRepository } from '../repositories/mikro-orm-video.repository'; + +@MultiORMEntity('video', { mikroOrmRepository: () => MikroOrmVideoRepository }) +export class Video extends TenantOrganizationBaseEntity implements IVideo { + /** + * Title of the video. + * This is a required field with a maximum length of 255 characters. + */ + @ApiProperty({ type: () => String, description: 'Title of the video' }) + @IsNotEmpty({ message: 'Title is required' }) + @IsString({ message: 'Title must be a string' }) + @Length(3, 255, { message: 'Title must be between 3 and 255 characters' }) + @Matches(/^[\p{L}\p{N}\s-]+$/u, { message: 'Title can contain letters, numbers, spaces, and hyphens from any language' }) + @MultiORMColumn() + title: string; + + /** + * Video file path or identifier. + * Must be a valid MP4 file with a valid name format. + */ + @ApiProperty({ type: () => String, description: 'Video file path or identifier' }) + @IsNotEmpty({ message: 'File is required' }) + @IsString({ message: 'File must be a string' }) + @Matches(/^[\w-]+\.(mp4)$/i, { + message: 'File must be a valid MP4 format and contain only letters, numbers, and hyphens' + }) + @MultiORMColumn() + file: string; + + /** + * Date when the video was recorded. + * This is optional and must be a valid past ISO 8601 date string. + */ + @ApiPropertyOptional({ type: () => 'timestamptz', description: 'Date when the video was recorded' }) + @IsOptional() + @IsDateString({}, { message: 'Recorded date must be a valid ISO 8601 date string' }) + @ValidateIf((o) => o.recordedAt && o.recordedAt <= new Date()) + @ColumnIndex() + @MultiORMColumn({ nullable: true }) + recordedAt?: Date; + + /** + * Duration of the video in seconds. + * This is optional and must be a positive number. + */ + @ApiProperty({ type: () => Number, description: 'Duration of the video in seconds' }) + @IsOptional() + @IsNumber({}, { message: 'Duration must be a number' }) + @Min(0, { message: 'Duration must be greater than or equal to 0' }) + @Transform(({ value }) => parseFloat(value), { toClassOnly: true }) + @MultiORMColumn({ nullable: true }) + duration?: number; + + /** + * Size of the video file in bytes. + * Optional with a maximum size limit of 10GB (10737418240 bytes). + */ + @ApiProperty({ type: () => Number, description: 'Size of the video file in bytes' }) + @IsOptional() + @IsNumber({}, { message: 'Size must be a number' }) + @Min(0, { message: 'Size must be greater than or equal to 0' }) + @Max(10737418240, { message: 'Size cannot exceed 10GB (10737418240 bytes)' }) + @Transform(({ value }) => parseFloat(value), { toClassOnly: true }) + @MultiORMColumn({ nullable: true }) + size?: number; + + /** + * Full URL to access the video. + * Optional and must be a valid HTTP or HTTPS URL. + */ + @ApiProperty({ type: () => String, description: 'Full URL to access the video' }) + @IsOptional() + @IsUrl( + { protocols: ['http', 'https'], require_protocol: true }, + { message: 'Full URL must be a valid HTTPS or HTTP URL' } + ) + @MultiORMColumn({ nullable: true }) + fullUrl?: string | null; + + /** + * Storage provider used for storing the video file. + * Optional and must match one of the predefined storage providers. + */ + @ApiPropertyOptional({ type: () => String, enum: FileStorageProviderEnum }) + @IsOptional() + @IsEnum(FileStorageProviderEnum, { + message: `Storage provider must be one of: ${Object.values(FileStorageProviderEnum).join(', ')}` + }) + @Exclude({ toPlainOnly: true }) + @ColumnIndex() + @MultiORMColumn({ type: 'simple-enum', nullable: true, enum: FileStorageProviderEnum }) + storageProvider?: FileStorageProvider; + + /** + * Description of the video. + * Optional with a maximum length of 1000 characters. + */ + @ApiProperty({ type: () => String, description: 'Video description' }) + @IsOptional() + @IsString({ message: 'Description must be a string' }) + @Length(0, 1000, { message: 'Description must not exceed 1000 characters' }) + @Matches(/^[\w\s.,!?-]*$/i, { + message: 'Description can only contain letters, numbers, spaces, and basic punctuation' + }) + @MultiORMColumn({ nullable: true }) + description?: string; + + /** + * Video resolution in the format WIDTH:HEIGHT. + * Optional and restricted to standard resolutions defined in VideoResolutionEnum. + */ + @ApiProperty({ + type: () => String, + enum: VideoResolutionEnum, + description: 'Video resolution in format WIDTH:HEIGHT (e.g., 1920:1080, 3840:2160)' + }) + @IsOptional() + @IsEnum(VideoResolutionEnum, { + message: `Resolution must be one of the following: ${Object.values(VideoResolutionEnum).join(', ')}` + }) + @MultiORMColumn({ nullable: true, default: VideoResolutionEnum.FullHD }) + resolution?: VideoResolutionEnum; + + /** + * Video codec used for encoding. + * Optional and restricted to standard codecs defined in VideoCodecEnum. + */ + @ApiPropertyOptional({ + type: () => String, + enum: VideoCodecEnum, + description: 'Video codec used for encoding (e.g., libx264, libx265, vp9)' + }) + @IsOptional() + @IsEnum(VideoCodecEnum, { + message: `Codec must be one of the following: ${Object.values(VideoCodecEnum).join(', ')}` + }) + @MultiORMColumn({ nullable: true, default: VideoCodecEnum.libx264 }) + codec?: VideoCodecEnum; + + /** + * Video frame rate in frames per second. + * Optional with a range from 1 to 240 fps. + */ + @ApiProperty({ type: () => Number, description: 'Video frame rate' }) + @IsOptional() + @IsNumber({}, { message: 'Frame rate must be a number' }) + @Min(1, { message: 'Frame rate must be at least 1 fps' }) + @Max(240, { message: 'Frame rate cannot exceed 240 fps' }) + @Transform(({ value }) => parseFloat(value), { toClassOnly: true }) + @MultiORMColumn({ nullable: true, default: 15 }) + frameRate?: number; + + /* + |-------------------------------------------------------------------------- + | @ManyToOne + |-------------------------------------------------------------------------- + */ + + /** + * Represents the associated TimeSlot for the video. + * This is an optional many-to-one relationship with cascading delete. + */ + @MultiORMManyToOne(() => TimeSlot, { + /** Specifies whether the relation column can have null values. */ + nullable: true, + + /** Specifies the action to take when the related entity is deleted. */ + onDelete: 'CASCADE' + }) + @JoinColumn() // Indicates this is the owning side of the relationship and specifies the join column. + timeSlot?: ITimeSlot; + + /** + * Represents the ID of the associated TimeSlot. + * This is an optional UUID (version 4) used as a foreign key reference. + */ + @ApiPropertyOptional({ type: () => String, description: 'The UUID of the associated TimeSlot' }) + @IsOptional() + @IsUUID('4', { message: 'TimeSlot ID must be a valid UUID v4' }) // Validates the ID is a proper UUID v4. + @RelationId((video: Video) => video.timeSlot) // Extracts the foreign key for the relationship. + @ColumnIndex() // Adds a database index for faster queries on the timeSlotId column. + @MultiORMColumn({ nullable: true, relationId: true }) // Marks it as a relation identifier. + timeSlotId?: ID; + + /** + * Represents the Employee who uploaded the video. + * This is an optional many-to-one relationship with cascading delete. + */ + @MultiORMManyToOne(() => Employee, { + /** Specifies whether the relation column can have null values. */ + nullable: true, + + /** Specifies the action to take when the related entity is deleted. */ + onDelete: 'CASCADE' + }) + @JoinColumn() // Indicates this is the owning side of the relationship and specifies the join column. + uploadedBy?: IEmployee; + + /** + * Represents the ID of the Employee who uploaded the video. + * This is an optional UUID (version 4) used as a foreign key reference. + */ + @RelationId((video: Video) => video.uploadedBy) // Extracts the foreign key for the relationship. + @ColumnIndex() // Adds a database index for faster queries on the uploadedById column. + @MultiORMColumn({ nullable: true, relationId: true }) // Marks it as a relation identifier. + uploadedById?: ID; +} diff --git a/packages/plugins/video-capture/src/lib/queries/get-video.query.ts b/packages/plugins/video-capture/src/lib/queries/get-video.query.ts new file mode 100644 index 00000000000..ec800d77230 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/queries/get-video.query.ts @@ -0,0 +1,8 @@ +import { IQuery } from '@nestjs/cqrs'; +import { FindOneOptions } from 'typeorm'; +import { IVideo } from '../video.model'; + +export class GetVideoQuery implements IQuery { + public static readonly type = '[Video] Get'; + constructor(public readonly id: string, public readonly options: FindOneOptions) {} +} diff --git a/packages/plugins/video-capture/src/lib/queries/get-videos.query.ts b/packages/plugins/video-capture/src/lib/queries/get-videos.query.ts new file mode 100644 index 00000000000..e0a930d9f6d --- /dev/null +++ b/packages/plugins/video-capture/src/lib/queries/get-videos.query.ts @@ -0,0 +1,9 @@ +import { IQuery } from '@nestjs/cqrs'; +import { PaginationParams } from '@gauzy/core'; +import { IVideo } from '../video.model'; + +export class GetVideosQuery implements IQuery { + public static readonly type = '[Videos] Get All'; + + constructor(public readonly params: PaginationParams) {} +} diff --git a/packages/plugins/video-capture/src/lib/queries/handlers/get-video.handler.ts b/packages/plugins/video-capture/src/lib/queries/handlers/get-video.handler.ts new file mode 100644 index 00000000000..b84972093a9 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/queries/handlers/get-video.handler.ts @@ -0,0 +1,35 @@ +import { NotFoundException } from '@nestjs/common'; +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { VideosService } from '../../services/videos.service'; +import { IVideo } from '../../video.model'; +import { GetVideoQuery } from '../get-video.query'; + +@QueryHandler(GetVideoQuery) +export class GetVideoQueryHandler implements IQueryHandler { + constructor(private readonly videosService: VideosService) {} + + /** + * Handles the `GetVideoQuery` to retrieve a video entity by its ID. + * + * @param query - The `GetVideoQuery` containing the ID of the video to be fetched and optional query options. + * + * @returns A promise resolving to the video entity (`IVideo`) if found. + * + * @throws {NotFoundException} If the video with the specified ID is not found. + */ + public async execute(query: GetVideoQuery): Promise { + // Destructure the query to extract the video ID and options + const { id, options = {} } = query; + + // Step 1: Fetch the video entity from the database + const video = await this.videosService.findOneByIdString(id, options); + + // Step 2: Throw a NotFoundException if the video does not exist + if (!video) { + throw new NotFoundException(`Video with ID ${id} not found.`); + } + + // Step 3: Return the video entity + return video; + } +} diff --git a/packages/plugins/video-capture/src/lib/queries/handlers/get-videos.handler.ts b/packages/plugins/video-capture/src/lib/queries/handlers/get-videos.handler.ts new file mode 100644 index 00000000000..2e4e413b514 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/queries/handlers/get-videos.handler.ts @@ -0,0 +1,25 @@ +import { IPagination } from '@gauzy/contracts'; +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { IVideo } from '../../video.model'; +import { VideosService } from '../../services/videos.service'; +import { GetVideosQuery } from '../get-videos.query'; + +@QueryHandler(GetVideosQuery) +export class GetVideosQueryHandler implements IQueryHandler { + constructor(private readonly videosService: VideosService) {} + + /** + * Handles the `GetVideosQuery` to retrieve a paginated list of video entities. + * + * @param query - The `GetVideosQuery` containing parameters for pagination and filtering. + * + * @returns A promise resolving to a paginated result (`IPagination`), including a list of videos and metadata. + */ + public async execute(query: GetVideosQuery): Promise> { + // Extract pagination and filter parameters from the query + const { params } = query; + + // Step 1: Fetch the paginated list of videos from the database + return this.videosService.paginate(params); + } +} diff --git a/packages/plugins/video-capture/src/lib/queries/handlers/index.ts b/packages/plugins/video-capture/src/lib/queries/handlers/index.ts new file mode 100644 index 00000000000..3a7b071e03e --- /dev/null +++ b/packages/plugins/video-capture/src/lib/queries/handlers/index.ts @@ -0,0 +1,4 @@ +import { GetVideoQueryHandler } from './get-video.handler'; +import { GetVideosQueryHandler } from './get-videos.handler'; + +export const QueryHandlers = [GetVideoQueryHandler, GetVideosQueryHandler]; diff --git a/packages/plugins/video-capture/src/lib/queries/index.ts b/packages/plugins/video-capture/src/lib/queries/index.ts new file mode 100644 index 00000000000..79fb221477c --- /dev/null +++ b/packages/plugins/video-capture/src/lib/queries/index.ts @@ -0,0 +1,2 @@ +export * from './get-video.query'; +export * from './get-videos.query'; diff --git a/packages/plugins/video-capture/src/lib/repositories/mikro-orm-video.repository.ts b/packages/plugins/video-capture/src/lib/repositories/mikro-orm-video.repository.ts new file mode 100644 index 00000000000..c9a0f1d9662 --- /dev/null +++ b/packages/plugins/video-capture/src/lib/repositories/mikro-orm-video.repository.ts @@ -0,0 +1,4 @@ +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; +import { Video } from '../entities/video.entity'; + +export class MikroOrmVideoRepository extends MikroOrmBaseEntityRepository