From 9b572eb1a93bb98fa5b6f9c960d9cdf095cd2ae8 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 31 Jan 2024 11:39:52 -0500 Subject: [PATCH 1/5] Add skeleton for video field transformers --- package-lock.json | 24 +++++++++++++++++++ packages/server/package.json | 1 + .../src/tag/transformers/field-transformer.ts | 18 ++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 packages/server/src/tag/transformers/field-transformer.ts diff --git a/package-lock.json b/package-lock.json index 38ff9b30..b644cf25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27142,6 +27142,7 @@ "dependencies": { "@apollo/subgraph": "^2.4.12", "@google-cloud/storage": "^7.7.0", + "@jsonforms/core": "^3.2.1", "@nestjs/apollo": "^12.0.7", "@nestjs/axios": "^3.0.1", "@nestjs/common": "^9.0.0", @@ -27190,6 +27191,17 @@ "typescript": "^4.7.4" } }, + "packages/server/node_modules/@jsonforms/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-3.2.1.tgz", + "integrity": "sha512-G5ldiWM2e5Vh5WrbjDKb0QJ1SG/39kxC48x2ufFKhfpDHibqhPaXegg5jmSBTNcB0ldE9jMmpKwxF6fi5VBCEA==", + "dependencies": { + "@types/json-schema": "^7.0.3", + "ajv": "^8.6.1", + "ajv-formats": "^2.1.0", + "lodash": "^4.17.21" + } + }, "packages/server/node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -44366,6 +44378,7 @@ "@apollo/subgraph": "^2.4.12", "@google-cloud/storage": "^7.7.0", "@graphql-codegen/typescript-graphql-request": "^6.1.0", + "@jsonforms/core": "*", "@nestjs/apollo": "^12.0.7", "@nestjs/axios": "^3.0.1", "@nestjs/cli": "^9.0.0", @@ -44411,6 +44424,17 @@ "typescript": "^4.7.4" }, "dependencies": { + "@jsonforms/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-3.2.1.tgz", + "integrity": "sha512-G5ldiWM2e5Vh5WrbjDKb0QJ1SG/39kxC48x2ufFKhfpDHibqhPaXegg5jmSBTNcB0ldE9jMmpKwxF6fi5VBCEA==", + "requires": { + "@types/json-schema": "^7.0.3", + "ajv": "^8.6.1", + "ajv-formats": "^2.1.0", + "lodash": "^4.17.21" + } + }, "typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", diff --git a/packages/server/package.json b/packages/server/package.json index 011457c0..3376aa8c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -25,6 +25,7 @@ "dependencies": { "@apollo/subgraph": "^2.4.12", "@google-cloud/storage": "^7.7.0", + "@jsonforms/core": "^3.2.1", "@nestjs/apollo": "^12.0.7", "@nestjs/axios": "^3.0.1", "@nestjs/common": "^9.0.0", diff --git a/packages/server/src/tag/transformers/field-transformer.ts b/packages/server/src/tag/transformers/field-transformer.ts new file mode 100644 index 00000000..86c4bcc1 --- /dev/null +++ b/packages/server/src/tag/transformers/field-transformer.ts @@ -0,0 +1,18 @@ +/** + * A field transformer handles converting and operating on fields of a tag. + * It handles adding in additional information, update intermediate data, + * and ensuring that the data meets any additional formatting requirements. + */ +export interface FieldTransformer { + transformField(field: any): Promise; +}; + +/** + * Tests to see if a given field should be transformed. Each `FieldTransformer` + * has a cooresponding `FieldTransformerTest` that determines if the field + * should be transformed. + * + * The rank is used to determine if one transformer should be used over another. + * The larger the rank, the higher the priority. + */ +export type FieldTransformerTest = (field: any) => [boolean, number]; From 6db395d8c32002a76511cb4af35a5306dc36da9d Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 31 Jan 2024 15:25:20 -0500 Subject: [PATCH 2/5] Ability to apply transformers to tag fields --- packages/server/src/study/study.model.ts | 4 +- .../server/src/tag/resolvers/tag.resolver.ts | 3 +- .../tag/services/tag-transformer.service.ts | 40 +++++++++++++++++++ .../server/src/tag/services/tag.service.ts | 11 +++-- packages/server/src/tag/tag.module.ts | 14 ++++++- .../transformers/field-transformer-factory.ts | 29 ++++++++++++++ .../src/tag/transformers/field-transformer.ts | 10 ++++- .../transformers/video-field-transformer.ts | 17 ++++++++ 8 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 packages/server/src/tag/services/tag-transformer.service.ts create mode 100644 packages/server/src/tag/transformers/field-transformer-factory.ts create mode 100644 packages/server/src/tag/transformers/video-field-transformer.ts diff --git a/packages/server/src/study/study.model.ts b/packages/server/src/study/study.model.ts index 576589d1..eaf53a30 100644 --- a/packages/server/src/study/study.model.ts +++ b/packages/server/src/study/study.model.ts @@ -2,7 +2,7 @@ import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { ObjectType, Field, ID } from '@nestjs/graphql'; import mongoose, { Document } from 'mongoose'; import JSON from 'graphql-type-json'; -import { Schema as JSONSchema } from 'jsonschema'; +import { Layout, JsonSchema as JSONSchema } from '@jsonforms/core'; /** Definition for the tag in JSON schema */ @Schema() @@ -14,7 +14,7 @@ export class TagSchema { @Prop({ type: mongoose.Schema.Types.Mixed, required: true }) @Field(() => JSON) - uiSchema: any; + uiSchema: Layout; } const TagSchemaSchema = SchemaFactory.createForClass(TagSchema); diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index 41fda85f..8262c264 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -53,7 +53,8 @@ export class TagResolver { @Args('data', { type: () => JSON }) data: any ): Promise { // TODO: Add user context and verify the correct user has completed the tag - await this.tagService.complete(tag, data); + const study = await this.studyPipe.transform(tag.study); + await this.tagService.complete(tag, data, study); return true; } diff --git a/packages/server/src/tag/services/tag-transformer.service.ts b/packages/server/src/tag/services/tag-transformer.service.ts new file mode 100644 index 00000000..925f3981 --- /dev/null +++ b/packages/server/src/tag/services/tag-transformer.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { Tag } from '../models/tag.model'; +import { Study } from '../../study/study.model'; +import { FieldTransformerFactory } from '../transformers/field-transformer-factory'; + +@Injectable() +export class TagTransformer { + constructor(private readonly fieldTransformerFactory: FieldTransformerFactory) {} + + /** + * Transforms the tag data. Takes in the whole tag and produces the modified + * tag data. + */ + async transformTagData(data: any, study: Study): Promise { + const transformedData: { [property: string] : any } = { }; + + const schema = study.tagSchema.dataSchema; + const uischema = study.tagSchema.uiSchema; + console.log(schema); + console.log(uischema); + + if (!schema.properties) { + return data; + } + + for (const field in schema.properties) { + const fieldSchema = schema.properties[field]; + const fieldUiSchema = uischema.elements.find((element) => (element as any).scope === `#/properties/${field}`); + if (!fieldUiSchema) { + throw new Error(`Could not find ui schema for field ${field}`); + } + + const transformer = this.fieldTransformerFactory.getTransformer(fieldUiSchema, fieldSchema); + const transformed = transformer ? await transformer.transformField(data[field], fieldUiSchema, fieldSchema) : data[field]; + transformedData[field] = transformed; + } + + return transformedData; + } +} diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 91762fdb..3950c6aa 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -6,13 +6,15 @@ import { Study } from '../../study/study.model'; import { Entry } from '../../entry/models/entry.model'; import { StudyService } from '../../study/study.service'; import { MongooseMiddlewareService } from '../../shared/service/mongoose-callback.service'; +import { TagTransformer } from './tag-transformer.service'; @Injectable() export class TagService { constructor( @InjectModel(Tag.name) private readonly tagModel: Model, private readonly studyService: StudyService, - middlewareService: MongooseMiddlewareService + middlewareService: MongooseMiddlewareService, + private readonly tagTransformService: TagTransformer ) { // Subscribe to study delete events middlewareService.register(Study.name, 'deleteOne', async (study: Study) => { @@ -99,7 +101,7 @@ export class TagService { } /** Store the data and mark the tag as complete */ - async complete(tag: Tag, data: any): Promise { + async complete(tag: Tag, data: any, study: Study): Promise { // If the tag is already complete, it cannot be saved again if (tag.complete) { throw new BadRequestException(`Cannot re-save tag data`); @@ -111,8 +113,11 @@ export class TagService { throw new BadRequestException(`Tag data does not match study schema`); } + // Handle any transformations + const transformed = await this.tagTransformService.transformTagData(data, study); + // Save the tag information and mark the tag as complete - await this.tagModel.findOneAndUpdate({ _id: tag._id }, { $set: { data, complete: true } }); + // await this.tagModel.findOneAndUpdate({ _id: tag._id }, { $set: { data, complete: true } }); } async isEntryEnabled(study: Study, entry: Entry) { diff --git a/packages/server/src/tag/tag.module.ts b/packages/server/src/tag/tag.module.ts index 06f1a3f7..eb8a5ddb 100644 --- a/packages/server/src/tag/tag.module.ts +++ b/packages/server/src/tag/tag.module.ts @@ -12,6 +12,9 @@ import { VideoField, VideoFieldSchema } from './models/video-field.model'; import { VideoFieldService } from './services/video-field.service'; import { VideoFieldResolver } from './resolvers/video-field.resolver'; import { GcpModule } from '../gcp/gcp.module'; +import { TagTransformer } from './services/tag-transformer.service'; +import { FieldTransformerFactory } from './transformers/field-transformer-factory'; +import { VideoFieldTransformer } from './transformers/video-field-transformer'; @Module({ imports: [ @@ -25,6 +28,15 @@ import { GcpModule } from '../gcp/gcp.module'; PermissionModule, GcpModule ], - providers: [TagService, TagResolver, TagPipe, VideoFieldService, VideoFieldResolver] + providers: [ + TagService, + TagResolver, + TagPipe, + VideoFieldService, + VideoFieldResolver, + TagTransformer, + FieldTransformerFactory, + VideoFieldTransformer + ] }) export class TagModule {} diff --git a/packages/server/src/tag/transformers/field-transformer-factory.ts b/packages/server/src/tag/transformers/field-transformer-factory.ts new file mode 100644 index 00000000..69dd4181 --- /dev/null +++ b/packages/server/src/tag/transformers/field-transformer-factory.ts @@ -0,0 +1,29 @@ +import { JsonSchema, UISchemaElement } from '@jsonforms/core'; +import { Injectable } from '@nestjs/common'; +import { FieldTransformer, FieldTransformerTest, NOT_APPLICABLE } from './field-transformer'; +import {VideoFieldTransformer, VideoFieldTransformerTest} from './video-field-transformer'; + +type FieldTransformerOptions = { tester: FieldTransformerTest, transformer: FieldTransformer }; + +@Injectable() +export class FieldTransformerFactory { + private readonly transformers: FieldTransformerOptions[] = [ + { tester: VideoFieldTransformerTest, transformer: this.videoFieldTransformer } + ]; + + constructor(private readonly videoFieldTransformer: VideoFieldTransformer) {} + + /** Get the transformer for the given field */ + getTransformer(uischema: UISchemaElement, schema: JsonSchema): FieldTransformer | null { + // Run the testers, discard non-matches, and sort by priority + const transformers = this.transformers + .filter(({ tester }) => tester(uischema, schema) !== NOT_APPLICABLE) + .sort((a, b) => b.tester(uischema, schema) - a.tester(uischema, schema)); + + // If there are any matches, return the first one + if (transformers.length > 0) { + return transformers[0].transformer; + } + return null; + } +} diff --git a/packages/server/src/tag/transformers/field-transformer.ts b/packages/server/src/tag/transformers/field-transformer.ts index 86c4bcc1..57dac882 100644 --- a/packages/server/src/tag/transformers/field-transformer.ts +++ b/packages/server/src/tag/transformers/field-transformer.ts @@ -1,10 +1,12 @@ +import { JsonSchema, UISchemaElement } from "@jsonforms/core"; + /** * A field transformer handles converting and operating on fields of a tag. * It handles adding in additional information, update intermediate data, * and ensuring that the data meets any additional formatting requirements. */ export interface FieldTransformer { - transformField(field: any): Promise; + transformField(field: unknown, uischema: UISchemaElement, schema: JsonSchema): Promise; }; /** @@ -14,5 +16,9 @@ export interface FieldTransformer { * * The rank is used to determine if one transformer should be used over another. * The larger the rank, the higher the priority. + * + * This is similar to the `RankedTester` interface in `@jsonforms/core`. */ -export type FieldTransformerTest = (field: any) => [boolean, number]; +export type FieldTransformerTest = (uischema: UISchemaElement, schema: JsonSchema) => number; +/** Number returned when a transformer does not apply to a given field */ +export const NOT_APPLICABLE = -1; diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts new file mode 100644 index 00000000..96b192ae --- /dev/null +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { FieldTransformer } from './field-transformer'; +import { JsonSchema, UISchemaElement } from '@jsonforms/core'; + +@Injectable() +export class VideoFieldTransformer implements FieldTransformer { + async transformField (data: unknown, uischema: UISchemaElement, schema: JsonSchema): Promise { + return data; + } +} + +export const VideoFieldTransformerTest = (uischema: UISchemaElement, _schema: JsonSchema) => { + if (uischema.options && uischema.options.customType && uischema.options.customType === 'video') { + return 10; + } + return -1; +}; From ca9416a3ae47b9e6820e8b5ff4216440b25f93b9 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 31 Jan 2024 17:09:25 -0500 Subject: [PATCH 3/5] Working field transformation --- .../server/src/tag/resolvers/tag.resolver.ts | 5 ++- .../tag/services/tag-transformer.service.ts | 14 ++++--- .../server/src/tag/services/tag.service.ts | 6 ++- .../src/tag/services/video-field.service.ts | 41 ++++++++++++++++++- packages/server/src/tag/tag.module.ts | 4 +- .../src/tag/transformers/field-transformer.ts | 3 +- .../transformers/video-field-transformer.ts | 20 +++++++-- 7 files changed, 78 insertions(+), 15 deletions(-) diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index 8262c264..1372585a 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -50,11 +50,12 @@ export class TagResolver { @Mutation(() => Boolean) async completeTag( @Args('tag', { type: () => ID }, TagPipe) tag: Tag, - @Args('data', { type: () => JSON }) data: any + @Args('data', { type: () => JSON }) data: any, + @TokenContext() user: TokenPayload ): Promise { // TODO: Add user context and verify the correct user has completed the tag const study = await this.studyPipe.transform(tag.study); - await this.tagService.complete(tag, data, study); + await this.tagService.complete(tag, data, study, user); return true; } diff --git a/packages/server/src/tag/services/tag-transformer.service.ts b/packages/server/src/tag/services/tag-transformer.service.ts index 925f3981..dbfe2b50 100644 --- a/packages/server/src/tag/services/tag-transformer.service.ts +++ b/packages/server/src/tag/services/tag-transformer.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { Tag } from '../models/tag.model'; import { Study } from '../../study/study.model'; import { FieldTransformerFactory } from '../transformers/field-transformer-factory'; +import { TokenPayload } from '../../jwt/token.dto'; @Injectable() export class TagTransformer { @@ -11,27 +11,31 @@ export class TagTransformer { * Transforms the tag data. Takes in the whole tag and produces the modified * tag data. */ - async transformTagData(data: any, study: Study): Promise { + async transformTagData(data: any, study: Study, user: TokenPayload): Promise { const transformedData: { [property: string] : any } = { }; const schema = study.tagSchema.dataSchema; const uischema = study.tagSchema.uiSchema; - console.log(schema); - console.log(uischema); if (!schema.properties) { return data; } for (const field in schema.properties) { + // Get the schema and ui schema for the field const fieldSchema = schema.properties[field]; const fieldUiSchema = uischema.elements.find((element) => (element as any).scope === `#/properties/${field}`); + + // If the field UI schema is not found, throw an error if (!fieldUiSchema) { throw new Error(`Could not find ui schema for field ${field}`); } + // Try to get the transformer for the field const transformer = this.fieldTransformerFactory.getTransformer(fieldUiSchema, fieldSchema); - const transformed = transformer ? await transformer.transformField(data[field], fieldUiSchema, fieldSchema) : data[field]; + + // Apply the transformation if present, otherwise just return the data + const transformed = transformer ? await transformer.transformField(data[field], fieldUiSchema, fieldSchema, user) : data[field]; transformedData[field] = transformed; } diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 3950c6aa..fda4c825 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -7,6 +7,7 @@ import { Entry } from '../../entry/models/entry.model'; import { StudyService } from '../../study/study.service'; import { MongooseMiddlewareService } from '../../shared/service/mongoose-callback.service'; import { TagTransformer } from './tag-transformer.service'; +import { TokenPayload } from '../../jwt/token.dto'; @Injectable() export class TagService { @@ -101,7 +102,7 @@ export class TagService { } /** Store the data and mark the tag as complete */ - async complete(tag: Tag, data: any, study: Study): Promise { + async complete(tag: Tag, data: any, study: Study, user: TokenPayload): Promise { // If the tag is already complete, it cannot be saved again if (tag.complete) { throw new BadRequestException(`Cannot re-save tag data`); @@ -114,7 +115,8 @@ export class TagService { } // Handle any transformations - const transformed = await this.tagTransformService.transformTagData(data, study); + const transformed = await this.tagTransformService.transformTagData(data, study, user); + console.log(transformed); // Save the tag information and mark the tag as complete // await this.tagModel.findOneAndUpdate({ _id: tag._id }, { $set: { data, complete: true } }); diff --git a/packages/server/src/tag/services/video-field.service.ts b/packages/server/src/tag/services/video-field.service.ts index 92df5398..e56e7abe 100644 --- a/packages/server/src/tag/services/video-field.service.ts +++ b/packages/server/src/tag/services/video-field.service.ts @@ -7,6 +7,10 @@ import { StudyService } from '../../study/study.service'; import { ConfigService } from '@nestjs/config'; import { GCP_STORAGE_PROVIDER } from '../../gcp/providers/storage.provider'; import { Storage, Bucket } from '@google-cloud/storage'; +import { Entry } from '../../entry/models/entry.model'; +import { EntryService } from '../../entry/services/entry.service'; +import {DatasetPipe} from '../../dataset/pipes/dataset.pipe'; +import {TokenPayload} from '../../jwt/token.dto'; @Injectable() export class VideoFieldService { @@ -20,7 +24,9 @@ export class VideoFieldService { @InjectModel(VideoField.name) private readonly videoFieldModel: Model, private readonly studyService: StudyService, private readonly configService: ConfigService, - @Inject(GCP_STORAGE_PROVIDER) private readonly storage: Storage + @Inject(GCP_STORAGE_PROVIDER) private readonly storage: Storage, + private readonly entryService: EntryService, + private readonly datasetPipe: DatasetPipe ) {} async saveVideoField(tag: Tag, field: string, index: number): Promise { @@ -62,6 +68,39 @@ export class VideoFieldService { return url; } + /** + * Move the video itself to the permanent storage location and create the + * cooresponding entry. + */ + async markComplete(videoFieldID: string, datasetID: string, user: TokenPayload): Promise { + const videoField = await this.videoFieldModel.findById(videoFieldID); + if (!videoField) { + throw new BadRequestException(`Video field ${videoFieldID} not found`); + } + + const dataset = await this.datasetPipe.transform(datasetID); + + // Make the entry + const entry = await this.entryService.create({ + entryID: 'TODO: Generate entry ID', + contentType: 'video/webm', + meta: {} + }, dataset, user); + + // Move the video to the permanent location + const source = this.bucket.file(videoField.bucketLocation); + const newLocation = `${dataset.bucketPrefix}/${entry._id}.webm`; + await source.move(newLocation); + await this.entryService.setBucketLocation(entry, newLocation); + entry.bucketLocation = newLocation; + + // Remove the video field + await this.videoFieldModel.deleteOne({ _id: videoField._id }); + + // Return the completed entry + return entry; + } + private getVideoFieldBucketLocation(tagID: string, field: string, index: number): string { return `${this.bucketPrefix}/${tagID}/${field}/${index}.${this.videoRecordFileType}`; } diff --git a/packages/server/src/tag/tag.module.ts b/packages/server/src/tag/tag.module.ts index eb8a5ddb..8885edaa 100644 --- a/packages/server/src/tag/tag.module.ts +++ b/packages/server/src/tag/tag.module.ts @@ -15,6 +15,7 @@ import { GcpModule } from '../gcp/gcp.module'; import { TagTransformer } from './services/tag-transformer.service'; import { FieldTransformerFactory } from './transformers/field-transformer-factory'; import { VideoFieldTransformer } from './transformers/video-field-transformer'; +import { DatasetModule } from '../dataset/dataset.module'; @Module({ imports: [ @@ -26,7 +27,8 @@ import { VideoFieldTransformer } from './transformers/video-field-transformer'; EntryModule, SharedModule, PermissionModule, - GcpModule + GcpModule, + DatasetModule ], providers: [ TagService, diff --git a/packages/server/src/tag/transformers/field-transformer.ts b/packages/server/src/tag/transformers/field-transformer.ts index 57dac882..25565028 100644 --- a/packages/server/src/tag/transformers/field-transformer.ts +++ b/packages/server/src/tag/transformers/field-transformer.ts @@ -1,4 +1,5 @@ import { JsonSchema, UISchemaElement } from "@jsonforms/core"; +import { TokenPayload } from "../../jwt/token.dto"; /** * A field transformer handles converting and operating on fields of a tag. @@ -6,7 +7,7 @@ import { JsonSchema, UISchemaElement } from "@jsonforms/core"; * and ensuring that the data meets any additional formatting requirements. */ export interface FieldTransformer { - transformField(field: unknown, uischema: UISchemaElement, schema: JsonSchema): Promise; + transformField(field: any, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload): Promise; }; /** diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts index 96b192ae..e60869b0 100644 --- a/packages/server/src/tag/transformers/video-field-transformer.ts +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -1,11 +1,25 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { FieldTransformer } from './field-transformer'; import { JsonSchema, UISchemaElement } from '@jsonforms/core'; +import { VideoFieldService } from '../services/video-field.service'; +import { TokenPayload } from '../../jwt/token.dto'; @Injectable() export class VideoFieldTransformer implements FieldTransformer { - async transformField (data: unknown, uischema: UISchemaElement, schema: JsonSchema): Promise { - return data; + constructor(private readonly videoFieldService: VideoFieldService) {} + + async transformField (data: string[], uischema: UISchemaElement, _schema: JsonSchema, user: TokenPayload): Promise { + const datasetID = uischema.options?.dataset; + if (!datasetID) { + throw new BadRequestException('Dataset ID not provided'); + } + + const videoFields = await Promise.all(data.map(async (videoFieldId) => { + const entry = await this.videoFieldService.markComplete(videoFieldId, datasetID, user); + return entry._id; + })); + + return videoFields; } } From fd3527248976a1d1d84aca13d3b8f8ef93fc60bd Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 31 Jan 2024 17:12:01 -0500 Subject: [PATCH 4/5] Fix formatting --- .../tag/services/tag-transformer.service.ts | 12 +++++++----- .../src/tag/services/video-field.service.ts | 18 +++++++++++------- .../transformers/field-transformer-factory.ts | 4 ++-- .../src/tag/transformers/field-transformer.ts | 6 +++--- .../transformers/video-field-transformer.ts | 17 ++++++++++++----- 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/server/src/tag/services/tag-transformer.service.ts b/packages/server/src/tag/services/tag-transformer.service.ts index dbfe2b50..a77ba154 100644 --- a/packages/server/src/tag/services/tag-transformer.service.ts +++ b/packages/server/src/tag/services/tag-transformer.service.ts @@ -8,11 +8,11 @@ export class TagTransformer { constructor(private readonly fieldTransformerFactory: FieldTransformerFactory) {} /** - * Transforms the tag data. Takes in the whole tag and produces the modified - * tag data. - */ + * Transforms the tag data. Takes in the whole tag and produces the modified + * tag data. + */ async transformTagData(data: any, study: Study, user: TokenPayload): Promise { - const transformedData: { [property: string] : any } = { }; + const transformedData: { [property: string]: any } = {}; const schema = study.tagSchema.dataSchema; const uischema = study.tagSchema.uiSchema; @@ -35,7 +35,9 @@ export class TagTransformer { const transformer = this.fieldTransformerFactory.getTransformer(fieldUiSchema, fieldSchema); // Apply the transformation if present, otherwise just return the data - const transformed = transformer ? await transformer.transformField(data[field], fieldUiSchema, fieldSchema, user) : data[field]; + const transformed = transformer + ? await transformer.transformField(data[field], fieldUiSchema, fieldSchema, user) + : data[field]; transformedData[field] = transformed; } diff --git a/packages/server/src/tag/services/video-field.service.ts b/packages/server/src/tag/services/video-field.service.ts index e56e7abe..09af6206 100644 --- a/packages/server/src/tag/services/video-field.service.ts +++ b/packages/server/src/tag/services/video-field.service.ts @@ -9,8 +9,8 @@ import { GCP_STORAGE_PROVIDER } from '../../gcp/providers/storage.provider'; import { Storage, Bucket } from '@google-cloud/storage'; import { Entry } from '../../entry/models/entry.model'; import { EntryService } from '../../entry/services/entry.service'; -import {DatasetPipe} from '../../dataset/pipes/dataset.pipe'; -import {TokenPayload} from '../../jwt/token.dto'; +import { DatasetPipe } from '../../dataset/pipes/dataset.pipe'; +import { TokenPayload } from '../../jwt/token.dto'; @Injectable() export class VideoFieldService { @@ -81,11 +81,15 @@ export class VideoFieldService { const dataset = await this.datasetPipe.transform(datasetID); // Make the entry - const entry = await this.entryService.create({ - entryID: 'TODO: Generate entry ID', - contentType: 'video/webm', - meta: {} - }, dataset, user); + const entry = await this.entryService.create( + { + entryID: 'TODO: Generate entry ID', + contentType: 'video/webm', + meta: {} + }, + dataset, + user + ); // Move the video to the permanent location const source = this.bucket.file(videoField.bucketLocation); diff --git a/packages/server/src/tag/transformers/field-transformer-factory.ts b/packages/server/src/tag/transformers/field-transformer-factory.ts index 69dd4181..77d6b8cc 100644 --- a/packages/server/src/tag/transformers/field-transformer-factory.ts +++ b/packages/server/src/tag/transformers/field-transformer-factory.ts @@ -1,9 +1,9 @@ import { JsonSchema, UISchemaElement } from '@jsonforms/core'; import { Injectable } from '@nestjs/common'; import { FieldTransformer, FieldTransformerTest, NOT_APPLICABLE } from './field-transformer'; -import {VideoFieldTransformer, VideoFieldTransformerTest} from './video-field-transformer'; +import { VideoFieldTransformer, VideoFieldTransformerTest } from './video-field-transformer'; -type FieldTransformerOptions = { tester: FieldTransformerTest, transformer: FieldTransformer }; +type FieldTransformerOptions = { tester: FieldTransformerTest; transformer: FieldTransformer }; @Injectable() export class FieldTransformerFactory { diff --git a/packages/server/src/tag/transformers/field-transformer.ts b/packages/server/src/tag/transformers/field-transformer.ts index 25565028..905e2981 100644 --- a/packages/server/src/tag/transformers/field-transformer.ts +++ b/packages/server/src/tag/transformers/field-transformer.ts @@ -1,5 +1,5 @@ -import { JsonSchema, UISchemaElement } from "@jsonforms/core"; -import { TokenPayload } from "../../jwt/token.dto"; +import { JsonSchema, UISchemaElement } from '@jsonforms/core'; +import { TokenPayload } from '../../jwt/token.dto'; /** * A field transformer handles converting and operating on fields of a tag. @@ -8,7 +8,7 @@ import { TokenPayload } from "../../jwt/token.dto"; */ export interface FieldTransformer { transformField(field: any, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload): Promise; -}; +} /** * Tests to see if a given field should be transformed. Each `FieldTransformer` diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts index e60869b0..6b40997b 100644 --- a/packages/server/src/tag/transformers/video-field-transformer.ts +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -8,16 +8,23 @@ import { TokenPayload } from '../../jwt/token.dto'; export class VideoFieldTransformer implements FieldTransformer { constructor(private readonly videoFieldService: VideoFieldService) {} - async transformField (data: string[], uischema: UISchemaElement, _schema: JsonSchema, user: TokenPayload): Promise { + async transformField( + data: string[], + uischema: UISchemaElement, + _schema: JsonSchema, + user: TokenPayload + ): Promise { const datasetID = uischema.options?.dataset; if (!datasetID) { throw new BadRequestException('Dataset ID not provided'); } - const videoFields = await Promise.all(data.map(async (videoFieldId) => { - const entry = await this.videoFieldService.markComplete(videoFieldId, datasetID, user); - return entry._id; - })); + const videoFields = await Promise.all( + data.map(async (videoFieldId) => { + const entry = await this.videoFieldService.markComplete(videoFieldId, datasetID, user); + return entry._id; + }) + ); return videoFields; } From 98912ab7b53dbea2326cb0a1b36b22b76c774ba1 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 31 Jan 2024 17:18:55 -0500 Subject: [PATCH 5/5] Add updating of tag data --- packages/server/src/tag/services/tag.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index fda4c825..97f02ced 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -116,10 +116,9 @@ export class TagService { // Handle any transformations const transformed = await this.tagTransformService.transformTagData(data, study, user); - console.log(transformed); // Save the tag information and mark the tag as complete - // await this.tagModel.findOneAndUpdate({ _id: tag._id }, { $set: { data, complete: true } }); + await this.tagModel.findOneAndUpdate({ _id: tag._id }, { $set: { data: transformed, complete: true } }); } async isEntryEnabled(study: Study, entry: Entry) {