diff --git a/db/migrations/910-add_picture_final_url.sql b/db/migrations/910-add_picture_final_url.sql new file mode 100644 index 00000000..8ea64063 --- /dev/null +++ b/db/migrations/910-add_picture_final_url.sql @@ -0,0 +1 @@ +ALTER TABLE pictures ADD COLUMN "finalUrl" TEXT; \ No newline at end of file diff --git a/packages/backend/openapi.json b/packages/backend/openapi.json index 5852837f..0b9438b4 100644 --- a/packages/backend/openapi.json +++ b/packages/backend/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"CR VIF API","description":"CR VIF API Documentation","version":"1.0"},"components":{"schemas":{}},"paths":{"/api/create-user":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"udap_id":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"}},"required":["name","udap_id","email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/login":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"}},"required":["email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/refresh-token":{"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"token","required":true},{"schema":{"type":"string"},"in":"query","name":"refreshToken","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/send-reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"}},"required":["email"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"temporaryLink":{"type":"string"},"newPassword":{"type":"string"}},"required":["temporaryLink","newPassword"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/udaps":{"get":{"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}}}}}}}},"/api/upload/image":{"post":{"responses":{"200":{"description":"Default Response"}}}},"/api/pdf/report":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"htmlString":{"type":"string"},"reportId":{"type":"string"},"recipients":{"type":"string"}},"required":["htmlString","reportId","recipients"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"string"}}}}}},"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"CR VIF API","description":"CR VIF API Documentation","version":"1.0"},"components":{"schemas":{}},"paths":{"/api/create-user":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"udap_id":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"}},"required":["name","udap_id","email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/login":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"}},"required":["email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/refresh-token":{"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"token","required":true},{"schema":{"type":"string"},"in":"query","name":"refreshToken","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/send-reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"}},"required":["email"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"temporaryLink":{"type":"string"},"newPassword":{"type":"string"}},"required":["temporaryLink","newPassword"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/udaps":{"get":{"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}}}}}}}},"/api/upload/image":{"post":{"responses":{"200":{"description":"Default Response"}}}},"/api/upload/picture":{"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true},{"schema":{"type":"string"},"in":"query","name":"pictureId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/api/upload/picture/{pictureId}/lines":{"post":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true},{"schema":{"type":"string"},"in":"path","name":"pictureId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"string"}}}}}}},"/api/pdf/report":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"htmlString":{"type":"string"},"reportId":{"type":"string"},"recipients":{"type":"string"}},"required":["htmlString","reportId","recipients"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"string"}}}}}},"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}}} \ No newline at end of file diff --git a/packages/backend/package.json b/packages/backend/package.json index 8bc82a00..af9a65e1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -46,6 +46,7 @@ "@react-pdf/renderer": "^3.4.2", "@sentry/node": "^7.70.0", "@sinclair/typebox": "^0.32.20", + "canvas": "^2.11.2", "date-fns": "^3.6.0", "debug": "^4.3.4", "dotenv": "^16.4.5", diff --git a/packages/backend/src/features/image.ts b/packages/backend/src/features/image.ts new file mode 100644 index 00000000..a3fe32f5 --- /dev/null +++ b/packages/backend/src/features/image.ts @@ -0,0 +1,61 @@ +import { db } from "../db/db"; +import canvas, { createCanvas, loadImage } from "canvas"; +import fs from "fs/promises"; + +export const getPictureWithLines = async ({ pictureId }: { pictureId: string }) => { + const picture = await db.pictures.findFirst({ + where: { id: pictureId }, + }); + + const pictureLines = await db.picture_lines.findFirst({ + where: { pictureId }, + }); + + const lines = JSON.parse(pictureLines?.lines || "[]"); + const buffer = await applyLinesToPicture({ pictureUrl: picture!.url!, lines }); + + await fs.writeFile("./test.png", buffer); + + return buffer; +}; + +export const applyLinesToPicture = async ({ + pictureUrl, + lines, +}: { + pictureUrl: string; + lines: Array<{ points: { x: number; y: number }[]; color: string }>; +}) => { + try { + const image = await loadImage(pictureUrl); + + const canvas = createCanvas(image.width, image.height); + const ctx = canvas.getContext("2d"); + + ctx.drawImage(image, 0, 0); + + ctx.lineWidth = 5; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + + lines.forEach((line) => { + ctx.strokeStyle = line.color; + if (line.points.length > 0) { + ctx.beginPath(); + + ctx.moveTo(line.points[0]!.x, line.points[0]!.y); + + for (let i = 1; i < line.points.length; i++) { + ctx.lineTo(line.points[i]!.x, line.points[i]!.y); + } + + ctx.stroke(); + } + }); + + return canvas.toBuffer("image/png"); + } catch (error) { + console.error("Error processing image:", error); + throw error; + } +}; diff --git a/packages/backend/src/routes/uploadRoutes.tsx b/packages/backend/src/routes/uploadRoutes.tsx index 9696e859..12fab7fb 100644 --- a/packages/backend/src/routes/uploadRoutes.tsx +++ b/packages/backend/src/routes/uploadRoutes.tsx @@ -32,7 +32,7 @@ export const uploadPlugin: FastifyPluginAsyncTypebox = async (fastify, _) => { debug("adding url to pic", id, "for report", reportId); - await db.pictures.create({ data: { id, url, reportId, createdAt: new Date() } }); + await db.pictures.create({ data: { id, url, reportId, createdAt: new Date(), finalUrl: url } }); // try { // await db.tmp_pictures.delete({ where: { id } }); // } catch (e) {} @@ -74,6 +74,30 @@ export const uploadPlugin: FastifyPluginAsyncTypebox = async (fastify, _) => { return buffer.toString("base64"); }, ); + + fastify.post( + "/picture/:pictureId/lines", + { + schema: { + params: Type.Object({ pictureId: Type.String() }), + body: Type.Object({ + lines: Type.Array( + Type.Object({ + points: Type.Array(Type.Object({ x: Type.Number(), y: Type.Number() })), + color: Type.String(), + }), + ), + }), + response: { 200: Type.String() }, + }, + }, + async (request) => { + const { pictureId } = request.params; + const { lines } = request.body; + + return request.services.upload.handleNotifyPictureLines({ pictureId, lines }); + }, + ); }; const getFileName = (file: MultipartFile) => { diff --git a/packages/backend/src/services/uploadService.ts b/packages/backend/src/services/uploadService.ts index d3d1ea4d..dc6830de 100644 --- a/packages/backend/src/services/uploadService.ts +++ b/packages/backend/src/services/uploadService.ts @@ -3,6 +3,8 @@ import { ENV } from "../envVars"; import { makeDebug } from "../features/debug"; import { AppError } from "../features/errors"; import { S3 } from "@aws-sdk/client-s3"; +import { applyLinesToPicture, getPictureWithLines } from "../features/image"; +import { db } from "../db/db"; const client = new S3Client({ endpoint: ENV.AWS_ENDPOINT, region: ENV.AWS_REGION }); const debug = makeDebug("upload"); @@ -87,7 +89,46 @@ export class UploadService { return Buffer.from(buffer); } + + async handleNotifyPictureLines({ + pictureId, + lines, + }: { + pictureId: string; + lines: Array<{ points: { x: number; y: number }[]; color: string }>; + }) { + debug("Handling picture lines", pictureId); + const picture = await db.pictures.findFirst({ + where: { id: pictureId }, + }); + if (!picture) throw new AppError(404, "Picture not found"); + + const buffer = await applyLinesToPicture({ pictureUrl: picture.url!, lines }); + + const name = getPictureName(pictureId, pictureId, Math.round(Date.now() / 1000)); + + const bucketUrl = `${ENV.MINIO_URL}/${ENV.MINIO_BUCKET}`; + + debug("Uploading picture to S3", pictureId); + await imageClient.putObject({ + Bucket: bucketUrl, + Key: name, + Body: buffer, + ACL: "public-read", + ContentType: "image/png", + }); + + debug("Picture uploaded", pictureId); + + const url = `${bucketUrl}/${name}`; + + await db.pictures.update({ where: { id: pictureId }, data: { finalUrl: url } }); + + debug(url); + return url; + } } export const getPDFName = (reportId: string) => `${reportId}/compte_rendu.pdf`; -export const getPictureName = (reportId: string, pictureId: string) => `${reportId}/pictures/${pictureId}.png`; +export const getPictureName = (reportId: string, pictureId: string, snapshot?: number) => + `${reportId}/pictures/${pictureId}${snapshot ? `_${snapshot}` : ""}.png`; diff --git a/packages/backend/test.png b/packages/backend/test.png new file mode 100644 index 00000000..b87d8fb4 Binary files /dev/null and b/packages/backend/test.png differ diff --git a/packages/electric-client/prisma/schema.prisma b/packages/electric-client/prisma/schema.prisma index 9007e24b..cd544acd 100644 --- a/packages/electric-client/prisma/schema.prisma +++ b/packages/electric-client/prisma/schema.prisma @@ -145,16 +145,26 @@ model pdf_snapshot { } model pictures { + id String @id + reportId String? + url String? + createdAt DateTime? @db.Timestamp(6) + finalUrl String? + picture_lines picture_lines[] + report report? @relation(fields: [reportId], references: [id], onDelete: Cascade, onUpdate: NoAction) +} + +model tmp_pictures { id String @id reportId String? - url String? createdAt DateTime? @db.Timestamp(6) report report? @relation(fields: [reportId], references: [id], onDelete: Cascade, onUpdate: NoAction) } -model tmp_pictures { +model picture_lines { id String @id - reportId String? + pictureId String? + lines String createdAt DateTime? @db.Timestamp(6) - report report? @relation(fields: [reportId], references: [id], onDelete: Cascade, onUpdate: NoAction) + pictures pictures? @relation(fields: [pictureId], references: [id], onDelete: Cascade, onUpdate: NoAction) } diff --git a/packages/electric-client/src/generated/client/index.ts b/packages/electric-client/src/generated/client/index.ts index a49fcff7..7d329a4a 100644 --- a/packages/electric-client/src/generated/client/index.ts +++ b/packages/electric-client/src/generated/client/index.ts @@ -23,7 +23,7 @@ export const Pdf_snapshotScalarFieldEnumSchema = z.enum(['id','report_id','html' export const Picture_linesScalarFieldEnumSchema = z.enum(['id','pictureId','lines','createdAt']); -export const PicturesScalarFieldEnumSchema = z.enum(['id','reportId','url','createdAt']); +export const PicturesScalarFieldEnumSchema = z.enum(['id','reportId','url','createdAt','finalUrl']); export const QueryModeSchema = z.enum(['default','insensitive']); @@ -120,6 +120,7 @@ export const PicturesSchema = z.object({ reportId: z.string().nullable(), url: z.string().nullable(), createdAt: z.coerce.date().nullable(), + finalUrl: z.string().nullable(), }) export type Pictures = z.infer @@ -322,6 +323,7 @@ export const PicturesSelectSchema: z.ZodType = z.object({ reportId: z.boolean().optional(), url: z.boolean().optional(), createdAt: z.boolean().optional(), + finalUrl: z.boolean().optional(), picture_lines: z.union([z.boolean(),z.lazy(() => Picture_linesFindManyArgsSchema)]).optional(), report: z.union([z.boolean(),z.lazy(() => ReportArgsSchema)]).optional(), _count: z.union([z.boolean(),z.lazy(() => PicturesCountOutputTypeArgsSchema)]).optional(), @@ -722,6 +724,7 @@ export const PicturesWhereInputSchema: z.ZodType = z. reportId: z.union([ z.lazy(() => StringNullableFilterSchema),z.string() ]).optional().nullable(), url: z.union([ z.lazy(() => StringNullableFilterSchema),z.string() ]).optional().nullable(), createdAt: z.union([ z.lazy(() => DateTimeNullableFilterSchema),z.coerce.date() ]).optional().nullable(), + finalUrl: z.union([ z.lazy(() => StringNullableFilterSchema),z.string() ]).optional().nullable(), picture_lines: z.lazy(() => Picture_linesListRelationFilterSchema).optional(), report: z.union([ z.lazy(() => ReportRelationFilterSchema),z.lazy(() => ReportWhereInputSchema) ]).optional().nullable(), }).strict(); @@ -731,6 +734,7 @@ export const PicturesOrderByWithRelationInputSchema: z.ZodType SortOrderSchema).optional(), url: z.lazy(() => SortOrderSchema).optional(), createdAt: z.lazy(() => SortOrderSchema).optional(), + finalUrl: z.lazy(() => SortOrderSchema).optional(), picture_lines: z.lazy(() => Picture_linesOrderByRelationAggregateInputSchema).optional(), report: z.lazy(() => ReportOrderByWithRelationInputSchema).optional() }).strict(); @@ -744,6 +748,7 @@ export const PicturesOrderByWithAggregationInputSchema: z.ZodType SortOrderSchema).optional(), url: z.lazy(() => SortOrderSchema).optional(), createdAt: z.lazy(() => SortOrderSchema).optional(), + finalUrl: z.lazy(() => SortOrderSchema).optional(), _count: z.lazy(() => PicturesCountOrderByAggregateInputSchema).optional(), _max: z.lazy(() => PicturesMaxOrderByAggregateInputSchema).optional(), _min: z.lazy(() => PicturesMinOrderByAggregateInputSchema).optional() @@ -757,6 +762,7 @@ export const PicturesScalarWhereWithAggregatesInputSchema: z.ZodType StringNullableWithAggregatesFilterSchema),z.string() ]).optional().nullable(), url: z.union([ z.lazy(() => StringNullableWithAggregatesFilterSchema),z.string() ]).optional().nullable(), createdAt: z.union([ z.lazy(() => DateTimeNullableWithAggregatesFilterSchema),z.coerce.date() ]).optional().nullable(), + finalUrl: z.union([ z.lazy(() => StringNullableWithAggregatesFilterSchema),z.string() ]).optional().nullable(), }).strict(); export const ReportWhereInputSchema: z.ZodType = z.object({ @@ -1358,6 +1364,7 @@ export const PicturesCreateInputSchema: z.ZodType = id: z.string(), url: z.string().optional().nullable(), createdAt: z.coerce.date().optional().nullable(), + finalUrl: z.string().optional().nullable(), picture_lines: z.lazy(() => Picture_linesCreateNestedManyWithoutPicturesInputSchema).optional(), report: z.lazy(() => ReportCreateNestedOneWithoutPicturesInputSchema).optional() }).strict(); @@ -1367,6 +1374,7 @@ export const PicturesUncheckedCreateInputSchema: z.ZodType Picture_linesUncheckedCreateNestedManyWithoutPicturesInputSchema).optional() }).strict(); @@ -1374,6 +1382,7 @@ export const PicturesUpdateInputSchema: z.ZodType = id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), picture_lines: z.lazy(() => Picture_linesUpdateManyWithoutPicturesNestedInputSchema).optional(), report: z.lazy(() => ReportUpdateOneWithoutPicturesNestedInputSchema).optional() }).strict(); @@ -1383,6 +1392,7 @@ export const PicturesUncheckedUpdateInputSchema: z.ZodType NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), picture_lines: z.lazy(() => Picture_linesUncheckedUpdateManyWithoutPicturesNestedInputSchema).optional() }).strict(); @@ -1390,13 +1400,15 @@ export const PicturesCreateManyInputSchema: z.ZodType = z.object({ id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); export const PicturesUncheckedUpdateManyInputSchema: z.ZodType = z.object({ @@ -1404,6 +1416,7 @@ export const PicturesUncheckedUpdateManyInputSchema: z.ZodType NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); export const ReportCreateInputSchema: z.ZodType = z.object({ @@ -2158,21 +2171,24 @@ export const PicturesCountOrderByAggregateInputSchema: z.ZodType SortOrderSchema).optional(), reportId: z.lazy(() => SortOrderSchema).optional(), url: z.lazy(() => SortOrderSchema).optional(), - createdAt: z.lazy(() => SortOrderSchema).optional() + createdAt: z.lazy(() => SortOrderSchema).optional(), + finalUrl: z.lazy(() => SortOrderSchema).optional() }).strict(); export const PicturesMaxOrderByAggregateInputSchema: z.ZodType = z.object({ id: z.lazy(() => SortOrderSchema).optional(), reportId: z.lazy(() => SortOrderSchema).optional(), url: z.lazy(() => SortOrderSchema).optional(), - createdAt: z.lazy(() => SortOrderSchema).optional() + createdAt: z.lazy(() => SortOrderSchema).optional(), + finalUrl: z.lazy(() => SortOrderSchema).optional() }).strict(); export const PicturesMinOrderByAggregateInputSchema: z.ZodType = z.object({ id: z.lazy(() => SortOrderSchema).optional(), reportId: z.lazy(() => SortOrderSchema).optional(), url: z.lazy(() => SortOrderSchema).optional(), - createdAt: z.lazy(() => SortOrderSchema).optional() + createdAt: z.lazy(() => SortOrderSchema).optional(), + finalUrl: z.lazy(() => SortOrderSchema).optional() }).strict(); export const DateTimeFilterSchema: z.ZodType = z.object({ @@ -3210,6 +3226,7 @@ export const PicturesCreateWithoutPicture_linesInputSchema: z.ZodType ReportCreateNestedOneWithoutPicturesInputSchema).optional() }).strict(); @@ -3217,7 +3234,8 @@ export const PicturesUncheckedCreateWithoutPicture_linesInputSchema: z.ZodType

= z.object({ @@ -3234,6 +3252,7 @@ export const PicturesUpdateWithoutPicture_linesInputSchema: z.ZodType StringFieldUpdateOperationsInputSchema) ]).optional(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), report: z.lazy(() => ReportUpdateOneWithoutPicturesNestedInputSchema).optional() }).strict(); @@ -3242,6 +3261,7 @@ export const PicturesUncheckedUpdateWithoutPicture_linesInputSchema: z.ZodType

NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); export const Picture_linesCreateWithoutPicturesInputSchema: z.ZodType = z.object({ @@ -3414,6 +3434,7 @@ export const PicturesCreateWithoutReportInputSchema: z.ZodType Picture_linesCreateNestedManyWithoutPicturesInputSchema).optional() }).strict(); @@ -3421,6 +3442,7 @@ export const PicturesUncheckedCreateWithoutReportInputSchema: z.ZodType Picture_linesUncheckedCreateNestedManyWithoutPicturesInputSchema).optional() }).strict(); @@ -3499,6 +3521,7 @@ export const PicturesScalarWhereInputSchema: z.ZodType StringNullableFilterSchema),z.string() ]).optional().nullable(), url: z.union([ z.lazy(() => StringNullableFilterSchema),z.string() ]).optional().nullable(), createdAt: z.union([ z.lazy(() => DateTimeNullableFilterSchema),z.coerce.date() ]).optional().nullable(), + finalUrl: z.union([ z.lazy(() => StringNullableFilterSchema),z.string() ]).optional().nullable(), }).strict(); export const UserUpsertWithoutReportInputSchema: z.ZodType = z.object({ @@ -4002,7 +4025,8 @@ export const Picture_linesUncheckedUpdateManyWithoutPicture_linesInputSchema: z. export const PicturesCreateManyReportInputSchema: z.ZodType = z.object({ id: z.string(), url: z.string().optional().nullable(), - createdAt: z.coerce.date().optional().nullable() + createdAt: z.coerce.date().optional().nullable(), + finalUrl: z.string().optional().nullable() }).strict(); export const Tmp_picturesCreateManyReportInputSchema: z.ZodType = z.object({ @@ -4014,6 +4038,7 @@ export const PicturesUpdateWithoutReportInputSchema: z.ZodType StringFieldUpdateOperationsInputSchema) ]).optional(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), picture_lines: z.lazy(() => Picture_linesUpdateManyWithoutPicturesNestedInputSchema).optional() }).strict(); @@ -4021,6 +4046,7 @@ export const PicturesUncheckedUpdateWithoutReportInputSchema: z.ZodType StringFieldUpdateOperationsInputSchema) ]).optional(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), picture_lines: z.lazy(() => Picture_linesUncheckedUpdateManyWithoutPicturesNestedInputSchema).optional() }).strict(); @@ -4028,6 +4054,7 @@ export const PicturesUncheckedUpdateManyWithoutPicturesInputSchema: z.ZodType StringFieldUpdateOperationsInputSchema) ]).optional(), url: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), createdAt: z.union([ z.coerce.date(),z.lazy(() => NullableDateTimeFieldUpdateOperationsInputSchema) ]).optional().nullable(), + finalUrl: z.union([ z.string(),z.lazy(() => NullableStringFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); export const Tmp_picturesUpdateWithoutReportInputSchema: z.ZodType = z.object({ @@ -5617,6 +5644,10 @@ export const tableSchemas = { [ "createdAt", "TIMESTAMP" + ], + [ + "finalUrl", + "TEXT" ] ]), relations: [ diff --git a/packages/electric-client/src/generated/client/migrations.ts b/packages/electric-client/src/generated/client/migrations.ts index a76a889b..2ba0cce1 100644 --- a/packages/electric-client/src/generated/client/migrations.ts +++ b/packages/electric-client/src/generated/client/migrations.ts @@ -272,5 +272,24 @@ export default [ "CREATE TRIGGER compensation_update_main_picture_lines_pictureId_into_oplog\n AFTER UPDATE ON \"main\".\"picture_lines\"\n WHEN 1 = (SELECT flag from _electric_trigger_settings WHERE namespace = 'main' AND tablename = 'picture_lines') AND\n 1 = (SELECT value from _electric_meta WHERE key = 'compensations')\nBEGIN\n INSERT INTO _electric_oplog (namespace, tablename, optype, primaryKey, newRow, oldRow, timestamp)\n SELECT 'main', 'pictures', 'COMPENSATION', json_patch('{}', json_object('id', \"id\")), json_object('id', \"id\"), NULL, NULL\n FROM \"main\".\"pictures\" WHERE \"id\" = new.\"pictureId\";\nEND;" ], "version": "909" + }, + { + "statements": [ + "ALTER TABLE \"pictures\" ADD COLUMN \"finalUrl\" TEXT;\n", + "INSERT OR IGNORE INTO _electric_trigger_settings (namespace, tablename, flag) VALUES ('main', 'pictures', 1);", + "DROP TRIGGER IF EXISTS update_ensure_main_pictures_primarykey;", + "CREATE TRIGGER update_ensure_main_pictures_primarykey\n BEFORE UPDATE ON \"main\".\"pictures\"\nBEGIN\n SELECT\n CASE\n WHEN old.\"id\" != new.\"id\" THEN\n \t\tRAISE (ABORT, 'cannot change the value of column id as it belongs to the primary key')\n END;\nEND;", + "DROP TRIGGER IF EXISTS insert_main_pictures_into_oplog;", + "CREATE TRIGGER insert_main_pictures_into_oplog\n AFTER INSERT ON \"main\".\"pictures\"\n WHEN 1 = (SELECT flag from _electric_trigger_settings WHERE namespace = 'main' AND tablename = 'pictures')\nBEGIN\n INSERT INTO _electric_oplog (namespace, tablename, optype, primaryKey, newRow, oldRow, timestamp)\n VALUES ('main', 'pictures', 'INSERT', json_patch('{}', json_object('id', new.\"id\")), json_object('createdAt', new.\"createdAt\", 'finalUrl', new.\"finalUrl\", 'id', new.\"id\", 'reportId', new.\"reportId\", 'url', new.\"url\"), NULL, NULL);\nEND;", + "DROP TRIGGER IF EXISTS update_main_pictures_into_oplog;", + "CREATE TRIGGER update_main_pictures_into_oplog\n AFTER UPDATE ON \"main\".\"pictures\"\n WHEN 1 = (SELECT flag from _electric_trigger_settings WHERE namespace = 'main' AND tablename = 'pictures')\nBEGIN\n INSERT INTO _electric_oplog (namespace, tablename, optype, primaryKey, newRow, oldRow, timestamp)\n VALUES ('main', 'pictures', 'UPDATE', json_patch('{}', json_object('id', new.\"id\")), json_object('createdAt', new.\"createdAt\", 'finalUrl', new.\"finalUrl\", 'id', new.\"id\", 'reportId', new.\"reportId\", 'url', new.\"url\"), json_object('createdAt', old.\"createdAt\", 'finalUrl', old.\"finalUrl\", 'id', old.\"id\", 'reportId', old.\"reportId\", 'url', old.\"url\"), NULL);\nEND;", + "DROP TRIGGER IF EXISTS delete_main_pictures_into_oplog;", + "CREATE TRIGGER delete_main_pictures_into_oplog\n AFTER DELETE ON \"main\".\"pictures\"\n WHEN 1 = (SELECT flag from _electric_trigger_settings WHERE namespace = 'main' AND tablename = 'pictures')\nBEGIN\n INSERT INTO _electric_oplog (namespace, tablename, optype, primaryKey, newRow, oldRow, timestamp)\n VALUES ('main', 'pictures', 'DELETE', json_patch('{}', json_object('id', old.\"id\")), NULL, json_object('createdAt', old.\"createdAt\", 'finalUrl', old.\"finalUrl\", 'id', old.\"id\", 'reportId', old.\"reportId\", 'url', old.\"url\"), NULL);\nEND;", + "DROP TRIGGER IF EXISTS compensation_insert_main_pictures_reportId_into_oplog;", + "CREATE TRIGGER compensation_insert_main_pictures_reportId_into_oplog\n AFTER INSERT ON \"main\".\"pictures\"\n WHEN 1 = (SELECT flag from _electric_trigger_settings WHERE namespace = 'main' AND tablename = 'pictures') AND\n 1 = (SELECT value from _electric_meta WHERE key = 'compensations')\nBEGIN\n INSERT INTO _electric_oplog (namespace, tablename, optype, primaryKey, newRow, oldRow, timestamp)\n SELECT 'main', 'report', 'COMPENSATION', json_patch('{}', json_object('id', \"id\")), json_object('id', \"id\"), NULL, NULL\n FROM \"main\".\"report\" WHERE \"id\" = new.\"reportId\";\nEND;", + "DROP TRIGGER IF EXISTS compensation_update_main_pictures_reportId_into_oplog;", + "CREATE TRIGGER compensation_update_main_pictures_reportId_into_oplog\n AFTER UPDATE ON \"main\".\"pictures\"\n WHEN 1 = (SELECT flag from _electric_trigger_settings WHERE namespace = 'main' AND tablename = 'pictures') AND\n 1 = (SELECT value from _electric_meta WHERE key = 'compensations')\nBEGIN\n INSERT INTO _electric_oplog (namespace, tablename, optype, primaryKey, newRow, oldRow, timestamp)\n SELECT 'main', 'report', 'COMPENSATION', json_patch('{}', json_object('id', \"id\")), json_object('id', \"id\"), NULL, NULL\n FROM \"main\".\"report\" WHERE \"id\" = new.\"reportId\";\nEND;" + ], + "version": "910" } ] \ No newline at end of file diff --git a/packages/electric-client/src/generated/client/pg-migrations.ts b/packages/electric-client/src/generated/client/pg-migrations.ts index 8116b664..74b53c1c 100644 --- a/packages/electric-client/src/generated/client/pg-migrations.ts +++ b/packages/electric-client/src/generated/client/pg-migrations.ts @@ -358,5 +358,30 @@ export default [ "CREATE TRIGGER compensation_update_public_picture_lines_pictureId_into_oplog\n AFTER UPDATE ON \"public\".\"picture_lines\"\n FOR EACH ROW\n EXECUTE FUNCTION compensation_update_public_picture_lines_pictureId_into_oplog_function();" ], "version": "909" + }, + { + "statements": [ + "ALTER TABLE pictures ADD COLUMN \"finalUrl\" TEXT", + "INSERT INTO \"public\".\"_electric_trigger_settings\" (\"namespace\", \"tablename\", \"flag\")\n VALUES ('public', 'pictures', 1)\n ON CONFLICT DO NOTHING;", + "DROP TRIGGER IF EXISTS update_ensure_public_pictures_primarykey ON \"public\".\"pictures\";", + "CREATE OR REPLACE FUNCTION update_ensure_public_pictures_primarykey_function()\nRETURNS TRIGGER AS $$\nBEGIN\n IF OLD.\"id\" IS DISTINCT FROM NEW.\"id\" THEN\n RAISE EXCEPTION 'Cannot change the value of column id as it belongs to the primary key';\n END IF;\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;", + "CREATE TRIGGER update_ensure_public_pictures_primarykey\n BEFORE UPDATE ON \"public\".\"pictures\"\n FOR EACH ROW\n EXECUTE FUNCTION update_ensure_public_pictures_primarykey_function();", + "DROP TRIGGER IF EXISTS insert_public_pictures_into_oplog ON \"public\".\"pictures\";", + " CREATE OR REPLACE FUNCTION insert_public_pictures_into_oplog_function()\n RETURNS TRIGGER AS $$\n BEGIN\n DECLARE\n flag_value INTEGER;\n BEGIN\n -- Get the flag value from _electric_trigger_settings\n SELECT flag INTO flag_value FROM \"public\"._electric_trigger_settings WHERE namespace = 'public' AND tablename = 'pictures';\n\n IF flag_value = 1 THEN\n -- Insert into _electric_oplog\n INSERT INTO \"public\"._electric_oplog (namespace, tablename, optype, \"primaryKey\", \"newRow\", \"oldRow\", timestamp)\n VALUES (\n 'public',\n 'pictures',\n 'INSERT',\n json_strip_nulls(json_build_object('id', new.\"id\")),\n jsonb_build_object('createdAt', new.\"createdAt\", 'finalUrl', new.\"finalUrl\", 'id', new.\"id\", 'reportId', new.\"reportId\", 'url', new.\"url\"),\n NULL,\n NULL\n );\n END IF;\n\n RETURN NEW;\n END;\n END;\n $$ LANGUAGE plpgsql;", + "CREATE TRIGGER insert_public_pictures_into_oplog\n AFTER INSERT ON \"public\".\"pictures\"\n FOR EACH ROW\n EXECUTE FUNCTION insert_public_pictures_into_oplog_function();", + "DROP TRIGGER IF EXISTS update_public_pictures_into_oplog ON \"public\".\"pictures\";", + " CREATE OR REPLACE FUNCTION update_public_pictures_into_oplog_function()\n RETURNS TRIGGER AS $$\n BEGIN\n DECLARE\n flag_value INTEGER;\n BEGIN\n -- Get the flag value from _electric_trigger_settings\n SELECT flag INTO flag_value FROM \"public\"._electric_trigger_settings WHERE namespace = 'public' AND tablename = 'pictures';\n\n IF flag_value = 1 THEN\n -- Insert into _electric_oplog\n INSERT INTO \"public\"._electric_oplog (namespace, tablename, optype, \"primaryKey\", \"newRow\", \"oldRow\", timestamp)\n VALUES (\n 'public',\n 'pictures',\n 'UPDATE',\n json_strip_nulls(json_build_object('id', new.\"id\")),\n jsonb_build_object('createdAt', new.\"createdAt\", 'finalUrl', new.\"finalUrl\", 'id', new.\"id\", 'reportId', new.\"reportId\", 'url', new.\"url\"),\n jsonb_build_object('createdAt', old.\"createdAt\", 'finalUrl', old.\"finalUrl\", 'id', old.\"id\", 'reportId', old.\"reportId\", 'url', old.\"url\"),\n NULL\n );\n END IF;\n\n RETURN NEW;\n END;\n END;\n $$ LANGUAGE plpgsql;", + "CREATE TRIGGER update_public_pictures_into_oplog\n AFTER UPDATE ON \"public\".\"pictures\"\n FOR EACH ROW\n EXECUTE FUNCTION update_public_pictures_into_oplog_function();", + "DROP TRIGGER IF EXISTS delete_public_pictures_into_oplog ON \"public\".\"pictures\";", + " CREATE OR REPLACE FUNCTION delete_public_pictures_into_oplog_function()\n RETURNS TRIGGER AS $$\n BEGIN\n DECLARE\n flag_value INTEGER;\n BEGIN\n -- Get the flag value from _electric_trigger_settings\n SELECT flag INTO flag_value FROM \"public\"._electric_trigger_settings WHERE namespace = 'public' AND tablename = 'pictures';\n\n IF flag_value = 1 THEN\n -- Insert into _electric_oplog\n INSERT INTO \"public\"._electric_oplog (namespace, tablename, optype, \"primaryKey\", \"newRow\", \"oldRow\", timestamp)\n VALUES (\n 'public',\n 'pictures',\n 'DELETE',\n json_strip_nulls(json_build_object('id', old.\"id\")),\n NULL,\n jsonb_build_object('createdAt', old.\"createdAt\", 'finalUrl', old.\"finalUrl\", 'id', old.\"id\", 'reportId', old.\"reportId\", 'url', old.\"url\"),\n NULL\n );\n END IF;\n\n RETURN NEW;\n END;\n END;\n $$ LANGUAGE plpgsql;", + "CREATE TRIGGER delete_public_pictures_into_oplog\n AFTER DELETE ON \"public\".\"pictures\"\n FOR EACH ROW\n EXECUTE FUNCTION delete_public_pictures_into_oplog_function();", + "DROP TRIGGER IF EXISTS compensation_insert_public_pictures_reportId_into_oplog ON \"public\".\"pictures\";", + " CREATE OR REPLACE FUNCTION compensation_insert_public_pictures_reportId_into_oplog_function()\n RETURNS TRIGGER AS $$\n BEGIN\n DECLARE\n flag_value INTEGER;\n meta_value INTEGER;\n BEGIN\n SELECT flag INTO flag_value FROM \"public\"._electric_trigger_settings WHERE namespace = 'public' AND tablename = 'pictures';\n\n SELECT value INTO meta_value FROM \"public\"._electric_meta WHERE key = 'compensations';\n\n IF flag_value = 1 AND meta_value = 1 THEN\n INSERT INTO \"public\"._electric_oplog (namespace, tablename, optype, \"primaryKey\", \"newRow\", \"oldRow\", timestamp)\n SELECT\n 'public',\n 'report',\n 'COMPENSATION',\n json_strip_nulls(json_strip_nulls(json_build_object('id', \"id\"))),\n jsonb_build_object('id', \"id\"),\n NULL,\n NULL\n FROM \"public\".\"report\"\n WHERE \"id\" = NEW.\"reportId\";\n END IF;\n\n RETURN NEW;\n END;\n END;\n $$ LANGUAGE plpgsql;", + "CREATE TRIGGER compensation_insert_public_pictures_reportId_into_oplog\n AFTER INSERT ON \"public\".\"pictures\"\n FOR EACH ROW\n EXECUTE FUNCTION compensation_insert_public_pictures_reportId_into_oplog_function();", + "DROP TRIGGER IF EXISTS compensation_update_public_pictures_reportId_into_oplog ON \"public\".\"pictures\";", + " CREATE OR REPLACE FUNCTION compensation_update_public_pictures_reportId_into_oplog_function()\n RETURNS TRIGGER AS $$\n BEGIN\n DECLARE\n flag_value INTEGER;\n meta_value INTEGER;\n BEGIN\n SELECT flag INTO flag_value FROM \"public\"._electric_trigger_settings WHERE namespace = 'public' AND tablename = 'pictures';\n\n SELECT value INTO meta_value FROM \"public\"._electric_meta WHERE key = 'compensations';\n\n IF flag_value = 1 AND meta_value = 1 THEN\n INSERT INTO \"public\"._electric_oplog (namespace, tablename, optype, \"primaryKey\", \"newRow\", \"oldRow\", timestamp)\n SELECT\n 'public',\n 'report',\n 'COMPENSATION',\n json_strip_nulls(json_strip_nulls(json_build_object('id', \"id\"))),\n jsonb_build_object('id', \"id\"),\n NULL,\n NULL\n FROM \"public\".\"report\"\n WHERE \"id\" = NEW.\"reportId\";\n END IF;\n\n RETURN NEW;\n END;\n END;\n $$ LANGUAGE plpgsql;", + "CREATE TRIGGER compensation_update_public_pictures_reportId_into_oplog\n AFTER UPDATE ON \"public\".\"pictures\"\n FOR EACH ROW\n EXECUTE FUNCTION compensation_update_public_pictures_reportId_into_oplog_function();" + ], + "version": "910" } ] \ No newline at end of file diff --git a/packages/electric-client/src/generated/client/prismaClient.d.ts b/packages/electric-client/src/generated/client/prismaClient.d.ts index 108406e2..bbb02899 100644 --- a/packages/electric-client/src/generated/client/prismaClient.d.ts +++ b/packages/electric-client/src/generated/client/prismaClient.d.ts @@ -118,6 +118,7 @@ export type PicturesPayload composites: {} } @@ -6582,6 +6583,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId: string | null url: string | null createdAt: Date | null + finalUrl: string | null } export type PicturesMaxAggregateOutputType = { @@ -6589,6 +6591,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId: string | null url: string | null createdAt: Date | null + finalUrl: string | null } export type PicturesCountAggregateOutputType = { @@ -6596,6 +6599,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId: number url: number createdAt: number + finalUrl: number _all: number } @@ -6605,6 +6609,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: true url?: true createdAt?: true + finalUrl?: true } export type PicturesMaxAggregateInputType = { @@ -6612,6 +6617,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: true url?: true createdAt?: true + finalUrl?: true } export type PicturesCountAggregateInputType = { @@ -6619,6 +6625,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: true url?: true createdAt?: true + finalUrl?: true _all?: true } @@ -6700,6 +6707,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId: string | null url: string | null createdAt: Date | null + finalUrl: string | null _count: PicturesCountAggregateOutputType | null _min: PicturesMinAggregateOutputType | null _max: PicturesMaxAggregateOutputType | null @@ -6724,6 +6732,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: boolean url?: boolean createdAt?: boolean + finalUrl?: boolean picture_lines?: boolean | Pictures$picture_linesArgs report?: boolean | ReportArgs _count?: boolean | PicturesCountOutputTypeArgs @@ -6734,6 +6743,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: boolean url?: boolean createdAt?: boolean + finalUrl?: boolean } export type PicturesInclude = { @@ -12579,7 +12589,8 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id: 'id', reportId: 'reportId', url: 'url', - createdAt: 'createdAt' + createdAt: 'createdAt', + finalUrl: 'finalUrl' }; export type PicturesScalarFieldEnum = (typeof PicturesScalarFieldEnum)[keyof typeof PicturesScalarFieldEnum] @@ -12921,6 +12932,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: StringNullableFilter | string | null url?: StringNullableFilter | string | null createdAt?: DateTimeNullableFilter | Date | string | null + finalUrl?: StringNullableFilter | string | null picture_lines?: Picture_linesListRelationFilter report?: XOR | null } @@ -12930,6 +12942,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: SortOrderInput | SortOrder url?: SortOrderInput | SortOrder createdAt?: SortOrderInput | SortOrder + finalUrl?: SortOrderInput | SortOrder picture_lines?: Picture_linesOrderByRelationAggregateInput report?: ReportOrderByWithRelationInput } @@ -12943,6 +12956,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: SortOrderInput | SortOrder url?: SortOrderInput | SortOrder createdAt?: SortOrderInput | SortOrder + finalUrl?: SortOrderInput | SortOrder _count?: PicturesCountOrderByAggregateInput _max?: PicturesMaxOrderByAggregateInput _min?: PicturesMinOrderByAggregateInput @@ -12956,6 +12970,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: StringNullableWithAggregatesFilter | string | null url?: StringNullableWithAggregatesFilter | string | null createdAt?: DateTimeNullableWithAggregatesFilter | Date | string | null + finalUrl?: StringNullableWithAggregatesFilter | string | null } export type ReportWhereInput = { @@ -13558,6 +13573,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id: string url?: string | null createdAt?: Date | string | null + finalUrl?: string | null picture_lines?: Picture_linesCreateNestedManyWithoutPicturesInput report?: ReportCreateNestedOneWithoutPicturesInput } @@ -13567,6 +13583,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: string | null url?: string | null createdAt?: Date | string | null + finalUrl?: string | null picture_lines?: Picture_linesUncheckedCreateNestedManyWithoutPicturesInput } @@ -13574,6 +13591,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id?: StringFieldUpdateOperationsInput | string url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null picture_lines?: Picture_linesUpdateManyWithoutPicturesNestedInput report?: ReportUpdateOneWithoutPicturesNestedInput } @@ -13583,6 +13601,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: NullableStringFieldUpdateOperationsInput | string | null url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null picture_lines?: Picture_linesUncheckedUpdateManyWithoutPicturesNestedInput } @@ -13591,12 +13610,14 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: string | null url?: string | null createdAt?: Date | string | null + finalUrl?: string | null } export type PicturesUpdateManyMutationInput = { id?: StringFieldUpdateOperationsInput | string url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null } export type PicturesUncheckedUpdateManyInput = { @@ -13604,6 +13625,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: NullableStringFieldUpdateOperationsInput | string | null url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null } export type ReportCreateInput = { @@ -14364,6 +14386,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: SortOrder url?: SortOrder createdAt?: SortOrder + finalUrl?: SortOrder } export type PicturesMaxOrderByAggregateInput = { @@ -14371,6 +14394,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: SortOrder url?: SortOrder createdAt?: SortOrder + finalUrl?: SortOrder } export type PicturesMinOrderByAggregateInput = { @@ -14378,6 +14402,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: SortOrder url?: SortOrder createdAt?: SortOrder + finalUrl?: SortOrder } export type DateTimeFilter = { @@ -15415,6 +15440,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id: string url?: string | null createdAt?: Date | string | null + finalUrl?: string | null report?: ReportCreateNestedOneWithoutPicturesInput } @@ -15423,6 +15449,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: string | null url?: string | null createdAt?: Date | string | null + finalUrl?: string | null } export type PicturesCreateOrConnectWithoutPicture_linesInput = { @@ -15439,6 +15466,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id?: StringFieldUpdateOperationsInput | string url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null report?: ReportUpdateOneWithoutPicturesNestedInput } @@ -15447,6 +15475,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: NullableStringFieldUpdateOperationsInput | string | null url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null } export type Picture_linesCreateWithoutPicturesInput = { @@ -15619,6 +15648,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id: string url?: string | null createdAt?: Date | string | null + finalUrl?: string | null picture_lines?: Picture_linesCreateNestedManyWithoutPicturesInput } @@ -15626,6 +15656,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id: string url?: string | null createdAt?: Date | string | null + finalUrl?: string | null picture_lines?: Picture_linesUncheckedCreateNestedManyWithoutPicturesInput } @@ -15704,6 +15735,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject reportId?: StringNullableFilter | string | null url?: StringNullableFilter | string | null createdAt?: DateTimeNullableFilter | Date | string | null + finalUrl?: StringNullableFilter | string | null } export type UserUpsertWithoutReportInput = { @@ -16208,6 +16240,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id: string url?: string | null createdAt?: Date | string | null + finalUrl?: string | null } export type Tmp_picturesCreateManyReportInput = { @@ -16219,6 +16252,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id?: StringFieldUpdateOperationsInput | string url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null picture_lines?: Picture_linesUpdateManyWithoutPicturesNestedInput } @@ -16226,6 +16260,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id?: StringFieldUpdateOperationsInput | string url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null picture_lines?: Picture_linesUncheckedUpdateManyWithoutPicturesNestedInput } @@ -16233,6 +16268,7 @@ export type InputJsonValue = null | string | number | boolean | InputJsonObject id?: StringFieldUpdateOperationsInput | string url?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null + finalUrl?: NullableStringFieldUpdateOperationsInput | string | null } export type Tmp_picturesUpdateWithoutReportInput = { diff --git a/packages/electric-client/src/generated/typebox/index.ts b/packages/electric-client/src/generated/typebox/index.ts index cf5f77aa..e2c29bdf 100644 --- a/packages/electric-client/src/generated/typebox/index.ts +++ b/packages/electric-client/src/generated/typebox/index.ts @@ -1,7 +1,7 @@ -export * from './atdatabases_migrations_applied'; export * from './atdatabases_migrations_appliedInput'; -export * from './atdatabases_migrations_versionInput'; export * from './atdatabases_migrations_version'; +export * from './atdatabases_migrations_applied'; +export * from './atdatabases_migrations_versionInput'; export * from './clause'; export * from './clauseInput'; export * from './report'; @@ -26,3 +26,5 @@ export * from './pictures'; export * from './picturesInput'; export * from './tmp_pictures'; export * from './tmp_picturesInput'; +export * from './picture_lines'; +export * from './picture_linesInput'; diff --git a/packages/electric-client/src/generated/typebox/picture_lines.ts b/packages/electric-client/src/generated/typebox/picture_lines.ts new file mode 100644 index 00000000..11b908eb --- /dev/null +++ b/packages/electric-client/src/generated/typebox/picture_lines.ts @@ -0,0 +1,19 @@ +import { Type, Static } from "@sinclair/typebox"; + +export const picture_lines = Type.Object({ + id: Type.String(), + pictureId: Type.Optional(Type.String()), + lines: Type.String(), + createdAt: Type.Optional(Type.String()), + pictures: Type.Optional( + Type.Object({ + id: Type.String(), + reportId: Type.Optional(Type.String()), + url: Type.Optional(Type.String()), + createdAt: Type.Optional(Type.String()), + finalUrl: Type.Optional(Type.String()), + }) + ), +}); + +export type picture_linesType = Static; diff --git a/packages/electric-client/src/generated/typebox/picture_linesInput.ts b/packages/electric-client/src/generated/typebox/picture_linesInput.ts new file mode 100644 index 00000000..2cdae0db --- /dev/null +++ b/packages/electric-client/src/generated/typebox/picture_linesInput.ts @@ -0,0 +1,19 @@ +import { Type, Static } from "@sinclair/typebox"; + +export const picture_linesInput = Type.Object({ + id: Type.String(), + pictureId: Type.Optional(Type.String()), + lines: Type.String(), + createdAt: Type.Optional(Type.String()), + pictures: Type.Optional( + Type.Object({ + id: Type.String(), + reportId: Type.Optional(Type.String()), + url: Type.Optional(Type.String()), + createdAt: Type.Optional(Type.String()), + finalUrl: Type.Optional(Type.String()), + }) + ), +}); + +export type picture_linesInputType = Static; diff --git a/packages/electric-client/src/generated/typebox/pictures.ts b/packages/electric-client/src/generated/typebox/pictures.ts index 044489b6..e1125026 100644 --- a/packages/electric-client/src/generated/typebox/pictures.ts +++ b/packages/electric-client/src/generated/typebox/pictures.ts @@ -5,6 +5,15 @@ export const pictures = Type.Object({ reportId: Type.Optional(Type.String()), url: Type.Optional(Type.String()), createdAt: Type.Optional(Type.String()), + finalUrl: Type.Optional(Type.String()), + picture_lines: Type.Array( + Type.Object({ + id: Type.String(), + pictureId: Type.Optional(Type.String()), + lines: Type.String(), + createdAt: Type.Optional(Type.String()), + }) + ), report: Type.Optional( Type.Object({ id: Type.String(), diff --git a/packages/electric-client/src/generated/typebox/picturesInput.ts b/packages/electric-client/src/generated/typebox/picturesInput.ts index 63d4544a..51b025eb 100644 --- a/packages/electric-client/src/generated/typebox/picturesInput.ts +++ b/packages/electric-client/src/generated/typebox/picturesInput.ts @@ -5,6 +5,15 @@ export const picturesInput = Type.Object({ reportId: Type.Optional(Type.String()), url: Type.Optional(Type.String()), createdAt: Type.Optional(Type.String()), + finalUrl: Type.Optional(Type.String()), + picture_lines: Type.Array( + Type.Object({ + id: Type.String(), + pictureId: Type.Optional(Type.String()), + lines: Type.String(), + createdAt: Type.Optional(Type.String()), + }) + ), report: Type.Optional( Type.Object({ id: Type.String(), diff --git a/packages/electric-client/src/generated/typebox/report.ts b/packages/electric-client/src/generated/typebox/report.ts index 2a2658f3..1c0bc842 100644 --- a/packages/electric-client/src/generated/typebox/report.ts +++ b/packages/electric-client/src/generated/typebox/report.ts @@ -30,6 +30,7 @@ export const report = Type.Object({ reportId: Type.Optional(Type.String()), url: Type.Optional(Type.String()), createdAt: Type.Optional(Type.String()), + finalUrl: Type.Optional(Type.String()), }) ), user: Type.Object({ diff --git a/packages/electric-client/src/generated/typebox/reportInput.ts b/packages/electric-client/src/generated/typebox/reportInput.ts index d6faca30..40f7e6c2 100644 --- a/packages/electric-client/src/generated/typebox/reportInput.ts +++ b/packages/electric-client/src/generated/typebox/reportInput.ts @@ -30,6 +30,7 @@ export const reportInput = Type.Object({ reportId: Type.Optional(Type.String()), url: Type.Optional(Type.String()), createdAt: Type.Optional(Type.String()), + finalUrl: Type.Optional(Type.String()), }) ), user: Type.Object({ diff --git a/packages/frontend/src/api.gen.ts b/packages/frontend/src/api.gen.ts index 6e9cdbf2..0a16c35b 100644 --- a/packages/frontend/src/api.gen.ts +++ b/packages/frontend/src/api.gen.ts @@ -150,6 +150,23 @@ export namespace Endpoints { parameters: never; response: unknown; }; + export type get_Apiuploadpicture = { + method: "GET"; + path: "/api/upload/picture"; + parameters: { + query: { reportId: string; pictureId: string }; + }; + response: Partial<{}>; + }; + export type post_ApiuploadpicturePictureIdlines = { + method: "POST"; + path: "/api/upload/picture/{pictureId}/lines"; + parameters: { + query: { reportId: string }; + path: { pictureId: string }; + }; + response: string; + }; export type post_Apipdfreport = { method: "POST"; path: "/api/pdf/report"; @@ -178,11 +195,13 @@ export type EndpointByMethod = { "/api/send-reset-password": Endpoints.post_ApisendResetPassword; "/api/reset-password": Endpoints.post_ApiresetPassword; "/api/upload/image": Endpoints.post_Apiuploadimage; + "/api/upload/picture/{pictureId}/lines": Endpoints.post_ApiuploadpicturePictureIdlines; "/api/pdf/report": Endpoints.post_Apipdfreport; }; get: { "/api/refresh-token": Endpoints.get_ApirefreshToken; "/api/udaps": Endpoints.get_Apiudaps; + "/api/upload/picture": Endpoints.get_Apiuploadpicture; "/api/pdf/report": Endpoints.get_Apipdfreport; }; }; diff --git a/packages/frontend/src/features/chips/useChipOptions.tsx b/packages/frontend/src/features/chips/useChipOptions.tsx index 589f9d02..2b878812 100644 --- a/packages/frontend/src/features/chips/useChipOptions.tsx +++ b/packages/frontend/src/features/chips/useChipOptions.tsx @@ -22,7 +22,7 @@ export const useChipOptions = (key?: string) => { // keep only the most specific chip for each value const chips = Object.values(grouped).map((value) => { - if (value.length > 1) return value.find((chip) => chip.udap_id !== "ALL")!; + if (value.length > 1) return value.find((chip) => chip.udap_id !== "ALL") ?? value[0]; return value[0]; }); @@ -32,6 +32,6 @@ export const useChipOptions = (key?: string) => { const transformChip = (chip: Clause_v2) => { return { ...chip, - text: chip.text?.replaceAll("\\n", "
").replace(/\n/g, "
"), + text: chip?.text?.replaceAll("\\n", "
").replace(/\n/g, "
"), }; }; diff --git a/packages/frontend/src/features/idb.ts b/packages/frontend/src/features/idb.ts index 482858af..2cc67e76 100644 --- a/packages/frontend/src/features/idb.ts +++ b/packages/frontend/src/features/idb.ts @@ -3,13 +3,18 @@ import { createStore, del } from "idb-keyval"; export const getPicturesStore = () => createStore("toSync", "images"); export const getToUploadStore = () => createStore("toUpload", "images"); export const getUploadStatusStore = () => createStore("uploadStatus", "images"); +export const getToPingStore = () => createStore("toPing", "images"); export const syncImages = async () => { - console.log("sync"); const registration = await navigator.serviceWorker.ready; await registration.sync.register("images"); }; +export const syncPictureLines = async () => { + const registration = await navigator.serviceWorker.ready; + await registration.sync.register("picture-lines"); +}; + export const deleteImageFromIdb = async (id: string) => { await del(id, getPicturesStore()); await del(id, getToUploadStore()); diff --git a/packages/frontend/src/features/upload/DrawingCanvas.tsx b/packages/frontend/src/features/upload/DrawingCanvas.tsx index fa55eabc..f5bd98b1 100644 --- a/packages/frontend/src/features/upload/DrawingCanvas.tsx +++ b/packages/frontend/src/features/upload/DrawingCanvas.tsx @@ -8,19 +8,21 @@ import { v4 } from "uuid"; import { Picture_lines } from "@cr-vif/electric-client/frontend"; type DrawEvent = React.MouseEvent | React.TouchEvent; - +export type Line = { points: Array<{ x: number; y: number }>; color: string }; export const ImageCanvas = ({ url, pictureId, lines: dbLines, containerRef, closeModal, + notifyPictureLines, }: { - lines: Array<{ points: Array<{ x: number; y: number }>; color: string }>; url: string; containerRef: any; pictureId: string; + lines: Array; closeModal: () => void; + notifyPictureLines: (args: { pictureId: string; lines: Array }) => void; }) => { const [tool, setTool] = useState("draw"); const [lines, setLines] = useState; color: string }>>([]); @@ -34,8 +36,6 @@ export const ImageCanvas = ({ const imageRef = useRef(null); const contextRef = useRef(null); - console.log(containerRef?.current?.getBoundingClientRect()); - const isDrawing = state === "drawing" || state === "hold"; useEffect(() => { @@ -268,6 +268,7 @@ export const ImageCanvas = ({ data: { id: v4(), pictureId, lines: JSON.stringify(lines) }, }); } + notifyPictureLines({ pictureId, lines }); closeModal(); }; diff --git a/packages/frontend/src/features/upload/UploadImage.tsx b/packages/frontend/src/features/upload/UploadImage.tsx index a8258970..4e6c02e6 100644 --- a/packages/frontend/src/features/upload/UploadImage.tsx +++ b/packages/frontend/src/features/upload/UploadImage.tsx @@ -1,20 +1,28 @@ import { useState, useRef, ChangeEvent, useEffect } from "react"; import { v4 } from "uuid"; import { db } from "../../db"; -import { deleteImageFromIdb, getPicturesStore, getToUploadStore, getUploadStatusStore, syncImages } from "../idb"; +import { + deleteImageFromIdb, + getPicturesStore, + getToPingStore, + getToUploadStore, + getUploadStatusStore, + syncImages, + syncPictureLines, +} from "../idb"; import { Box, Flex, Grid, Stack, styled } from "#styled-system/jsx"; import { InputGroup } from "#components/InputGroup.tsx"; import { cx } from "#styled-system/css"; import { Tmp_pictures, Pictures, Report } from "@cr-vif/electric-client/frontend"; import { useMutation, useQuery } from "@tanstack/react-query"; import { useLiveQuery } from "electric-sql/react"; -import { get, set } from "idb-keyval"; +import { del, get, set } from "idb-keyval"; import { useFormContext } from "react-hook-form"; import Badge from "@codegouvfr/react-dsfr/Badge"; import Button from "@codegouvfr/react-dsfr/Button"; import { css } from "#styled-system/css"; import { createModal } from "@codegouvfr/react-dsfr/Modal"; -import { ImageCanvas } from "./DrawingCanvas"; +import { ImageCanvas, Line } from "./DrawingCanvas"; import { api } from "../../api"; const modal = createModal({ @@ -26,6 +34,19 @@ export const UploadImage = ({ reportId }: { reportId: string }) => { const [statusMap, setStatusMap] = useState>({}); const [selectedPicture, setSelectedPicture] = useState<{ id: string; url: string } | null>(null); + const notifyPictureLines = useMutation(async ({ pictureId, lines }: { pictureId: string; lines: Array }) => { + try { + // @ts-ignore + const result = await api.post(`/api/upload/picture/${pictureId}/lines`, { body: { lines } }); + await del(pictureId, getToPingStore()); + + return result; + } catch (e) { + await set(pictureId, true, getToPingStore()); + syncPictureLines(); + } + }); + // const linesQuery = useLiveQuery(db.picture_lines.liveMany({ where: { pictureId: selectedPicture?.id } })); const linesQuery = useQuery({ @@ -44,8 +65,6 @@ export const UploadImage = ({ reportId }: { reportId: string }) => { const picId = v4(); const buffer = await getArrayBufferFromBlob(file); - console.log("oui"); - await db.tmp_pictures.create({ data: { id: picId, reportId, createdAt: new Date() } }); await set(picId, buffer, getPicturesStore()); @@ -105,6 +124,7 @@ export const UploadImage = ({ reportId }: { reportId: string }) => { {selectedPicture ? ( setSelectedPicture(null)} + notifyPictureLines={notifyPictureLines.mutate} pictureId={selectedPicture.id} url={selectedPicture.url} containerRef={containerRef} @@ -213,12 +233,14 @@ const PictureThumbnail = ({ return "ok"; }); + const canvasRef = useRef(null); + const bgUrlQuery = useQuery({ queryKey: ["picture", picture.id, picture.url], queryFn: async () => { + // if (picture.url) return picture.finalUrl ?? picture.url; const buffer = await get(picture.id, getPicturesStore()); if (!buffer) return picture.url; - const blob = new Blob([buffer], { type: "image/png" }); return URL.createObjectURL(blob); @@ -226,6 +248,8 @@ const PictureThumbnail = ({ refetchOnWindowFocus: false, }); + const pictureLines = useLiveQuery(db.picture_lines.liveMany({ where: { pictureId: picture.id } })); + const idbStatusQuery = useQuery({ queryKey: ["picture-status", picture.id], queryFn: async () => { @@ -235,6 +259,64 @@ const PictureThumbnail = ({ enabled: !status, }); + useEffect(() => { + drawCanvas(); + }, [bgUrlQuery.data, pictureLines.results]); + + const drawCanvas = () => { + if (!canvasRef.current) return; + if (!bgUrlQuery.data) return; + if (!pictureLines.results) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d")!; + + const image = new Image(); + image.src = bgUrlQuery.data; + + const dpr = window.devicePixelRatio || 1; + canvas.width = 180 * dpr; + canvas.height = 130 * dpr; + + ctx.scale(dpr, dpr); + + image.onload = () => { + const scaleX = 180 / image.width; + const scaleY = 130 / image.height; + console.log(scaleX, scaleY); + const initialScale = Math.min(scaleX, scaleY) * 1.5; + + const xOffset = (180 - image.width * initialScale) / 2; + const yOffset = (130 - image.height * initialScale) / 2; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.save(); + + ctx.translate(xOffset, yOffset); + ctx.scale(initialScale, initialScale); + + ctx.drawImage(image, 0, 0, image.width, image.height); + + const lines = JSON.parse(pictureLines.results?.[0]?.lines ?? "[]"); + + ctx.lineWidth = 5; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + + lines.forEach((line: any) => { + ctx.beginPath(); + ctx.strokeStyle = line.color; + if (line.points.length > 0) { + ctx.moveTo(line.points[0].x, line.points[0].y); + for (let i = 1; i < line.points.length; i++) { + ctx.lineTo(line.points[i].x, line.points[i].y); + } + ctx.stroke(); + } + }); + }; + }; + const finalStatus = picture.url ? "success" : status ?? idbStatusQuery.data ?? "uploading"; const bgUrl = bgUrlQuery.data; @@ -244,15 +326,16 @@ const PictureThumbnail = ({ {/* */} + { diff --git a/packages/frontend/src/routes/pdf.$reportId.tsx b/packages/frontend/src/routes/pdf.$reportId.tsx index 05e3f2b4..87a7f296 100644 --- a/packages/frontend/src/routes/pdf.$reportId.tsx +++ b/packages/frontend/src/routes/pdf.$reportId.tsx @@ -385,6 +385,8 @@ export const WithReport = ({ const { editor } = useContext(TextEditorContext); const [htmlString] = useState(initialHtmlString); + console.log(report); + useEffect(() => { if (!editor) return; diff --git a/packages/frontend/src/service-worker/sw.ts b/packages/frontend/src/service-worker/sw.ts index 15135b84..46fe30eb 100644 --- a/packages/frontend/src/service-worker/sw.ts +++ b/packages/frontend/src/service-worker/sw.ts @@ -1,6 +1,6 @@ import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from "workbox-precaching"; import { apiStore, createApiClientWithUrl, getTokenFromIdb } from "../api"; -import { getPicturesStore, getToUploadStore, getUploadStatusStore } from "../features/idb"; +import { getPicturesStore, getToPingStore, getToUploadStore, getUploadStatusStore } from "../features/idb"; import { get, keys, del, set } from "idb-keyval"; import { NavigationRoute, registerRoute } from "workbox-routing"; import { isDev } from "../envVars"; @@ -27,10 +27,43 @@ if (!isDev) { const broadcastChannel = new BroadcastChannel("sw-messages"); self.addEventListener("sync", async (event: any) => { - broadcastChannel.postMessage({ type: "sync" }); - event.waitUntil(syncMissingPictures()); + if (event.tag === "images") { + event.waitUntil(syncMissingPictures()); + } else if (event.tag === "picture-lines") { + event.waitUntil(syncPictureLines()); + } }); +const syncPictureLines = async () => { + try { + const token = await getTokenFromIdb(); + if (!token) return void console.log("no token"); + + const pictureIds = await keys(getToPingStore()); + + console.log("syncing", pictureIds.length, "picture lines"); + + const url = await get("url", apiStore); + if (!url) return void console.error("no backend url in service worker"); + + const api = createApiClientWithUrl(url); + + for (let i = 0; i < pictureIds.length; i++) { + const picId = pictureIds[i]; + console.log("syncing picture lines for", picId); + + // @ts-ignore + await api.post(`/api/upload/picture/${pictureId}/lines`, { + header: { Authorization: `Bearer ${token}` }, + } as any); + + await del(picId, getToPingStore()); + } + } catch (e) { + console.error("sync error", e); + } +}; + const syncMissingPictures = async () => { try { const token = await getTokenFromIdb(); diff --git a/packages/pdf/src/report.tsx b/packages/pdf/src/report.tsx index 8e85b1fd..3fabb9ad 100644 --- a/packages/pdf/src/report.tsx +++ b/packages/pdf/src/report.tsx @@ -9,6 +9,7 @@ Font.registerHyphenationCallback((word) => { }); export const ReportPDFDocument = ({ udap, htmlString, images, pictures }: ReportPDFDocumentProps) => { + console.log(pictures); return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10d45737..3abcffe1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,6 +76,9 @@ importers: '@sinclair/typebox': specifier: ^0.32.20 version: 0.32.20 + canvas: + specifier: ^2.11.2 + version: 2.11.2 date-fns: specifier: ^3.6.0 version: 3.6.0 @@ -1359,7 +1362,7 @@ packages: '@babel/traverse': 7.24.1 '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2788,7 +2791,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.24.7 '@babel/types': 7.24.7 - debug: 4.3.4 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4062,7 +4065,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.7 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -4245,7 +4248,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.4 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4370,7 +4373,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - optional: true /@mikecousins/react-pdf@7.1.0(pdfjs-dist@4.3.136)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-uwJMgu+PQ5ULjBmuVD+R1gk1VpEjErDHxBgBrOoEBWmOk+DjoMxPV4ymPCWo7U9zLV5KJ+ppUGFa0w68fJ5yfw==} @@ -7091,7 +7093,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.3) '@typescript-eslint/utils': 7.5.0(eslint@8.57.0)(typescript@5.4.3) - debug: 4.3.4 + debug: 4.3.7 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.3) typescript: 5.4.3 @@ -7115,7 +7117,7 @@ packages: dependencies: '@typescript-eslint/types': 7.5.0 '@typescript-eslint/visitor-keys': 7.5.0 - debug: 4.3.4 + debug: 4.3.7 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -8458,7 +8460,6 @@ packages: /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} requiresBuild: true - optional: true /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -8476,6 +8477,7 @@ packages: /acorn-import-assertions@1.9.0(acorn@8.11.3): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 dependencies: @@ -8498,7 +8500,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4 + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -8609,7 +8611,6 @@ packages: /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} requiresBuild: true - optional: true /archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} @@ -8652,7 +8653,6 @@ packages: dependencies: delegates: 1.0.0 readable-stream: 3.6.2 - optional: true /arg@5.0.1: resolution: {integrity: sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==} @@ -8750,7 +8750,7 @@ packages: dependencies: '@fastify/error': 3.4.1 archy: 1.0.0 - debug: 4.3.4 + debug: 4.3.7 fastq: 1.17.1 transitivePeerDependencies: - supports-color @@ -9041,7 +9041,6 @@ packages: transitivePeerDependencies: - encoding - supports-color - optional: true /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -9261,7 +9260,6 @@ packages: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true requiresBuild: true - optional: true /commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} @@ -9315,7 +9313,6 @@ packages: /console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} requiresBuild: true - optional: true /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} @@ -9695,7 +9692,6 @@ packages: optional: true dependencies: ms: 2.1.3 - dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -9716,7 +9712,6 @@ packages: requiresBuild: true dependencies: mimic-response: 2.1.0 - optional: true /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} @@ -9847,7 +9842,6 @@ packages: /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} requiresBuild: true - optional: true /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} @@ -11242,7 +11236,6 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wide-align: 1.1.5 - optional: true /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} @@ -11466,7 +11459,6 @@ packages: /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} requiresBuild: true - optional: true /has-yarn@2.1.0: resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} @@ -11548,7 +11540,7 @@ packages: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.7 transitivePeerDependencies: - supports-color dev: true @@ -11558,7 +11550,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -12709,7 +12701,6 @@ packages: resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} engines: {node: '>=8'} requiresBuild: true - optional: true /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} @@ -12774,7 +12765,6 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} requiresBuild: true - optional: true /minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} @@ -12858,7 +12848,6 @@ packages: /nan@2.19.0: resolution: {integrity: sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==} requiresBuild: true - optional: true /nano-css@5.6.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==} @@ -12957,7 +12946,6 @@ packages: requiresBuild: true dependencies: abbrev: 1.1.1 - optional: true /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -13071,7 +13059,6 @@ packages: console-control-strings: 1.1.0 gauge: 3.0.2 set-blocking: 2.0.0 - optional: true /nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -14762,7 +14749,6 @@ packages: decompress-response: 4.2.1 once: 1.4.0 simple-concat: 1.0.1 - optional: true /simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} @@ -15226,7 +15212,6 @@ packages: minizlib: 2.1.2 mkdirp: 1.0.4 yallist: 4.0.0 - optional: true /tcp-port-used@1.0.2: resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==} @@ -16193,7 +16178,6 @@ packages: requiresBuild: true dependencies: string-width: 4.2.3 - optional: true /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}