From e25129960eef7dc4fabad005d2550cdf4d656d24 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 11:02:04 -0400 Subject: [PATCH 1/7] needed npx --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2334f03..83b22f21 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "fly:console:prisma:studio:proxy:prod": "npm run fly:console:prisma:studio:proxy -- --env=production", "fly:console:prisma:studio:proxy:staging": "npm run fly:console:prisma:studio:proxy -- --env=staging", "fly:console:prisma:studio:proxy": "./scripts/fly.io/app-console-prisma-studio-proxy.sh", - "data:migrate": "tsx ./prisma/data-migrations/populate-design-attributes-by-type.ts", + "data:migrate": "npx tsx ./prisma/data-migrations/populate-design-attributes-by-type.ts", "prepare": "husky install" }, "eslintIgnore": [ From 43006588c3a278412e668e6dddb21f8562a477db Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 11:29:54 -0400 Subject: [PATCH 2/7] layout --- app/models/design/design.server.ts | 10 +++- .../design/layout/layout.delete.server.ts | 13 +++++ app/models/design/layout/layout.get.server.ts | 52 +++++++++++++++++ app/models/design/layout/layout.server.ts | 23 ++++++++ .../design/layout/layout.update.server.ts | 20 +++++++ .../layout/layout.update.style.server.ts | 53 +++++++++++++++++ app/models/design/layout/utils.ts | 39 +++++++++++++ app/models/design/utils.ts | 3 + app/schema/design/layout.ts | 57 +++++++++++++++++++ .../populate-design-attributes-by-type.ts | 34 +++++++++++ 10 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 app/models/design/layout/layout.delete.server.ts create mode 100644 app/models/design/layout/layout.get.server.ts create mode 100644 app/models/design/layout/layout.server.ts create mode 100644 app/models/design/layout/layout.update.server.ts create mode 100644 app/models/design/layout/layout.update.style.server.ts create mode 100644 app/models/design/layout/utils.ts create mode 100644 app/schema/design/layout.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index 58f8de65..f6eef028 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -27,6 +27,10 @@ import { type IDesignAttributesFill, type IDesignFill, } from './fill/fill.server' +import { + type IDesignLayout, + type IDesignAttributesLayout, +} from './layout/layout.server' import { type IDesignStroke, type IDesignAttributesStroke, @@ -51,7 +55,10 @@ export interface IDesign extends BaseDesign { // when adding attributes to a design type, // make sure it starts as optional or is set to a default value // for when parsing the design from the deserializer -export type IDesignAttributes = IDesignAttributesFill | IDesignAttributesStroke +export type IDesignAttributes = + | IDesignAttributesFill + | IDesignAttributesLayout + | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { type: designTypeEnum @@ -64,6 +71,7 @@ export interface IDesignParsed extends BaseDesign { // TODO: replace with this ^^ export type IDesignByType = { designFills: IDesignFill[] + designLayoutss: IDesignLayout[] designStroke: IDesignStroke[] } diff --git a/app/models/design/layout/layout.delete.server.ts b/app/models/design/layout/layout.delete.server.ts new file mode 100644 index 00000000..429947eb --- /dev/null +++ b/app/models/design/layout/layout.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignLayout } from './layout.server' + +export interface IDesignLayoutDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignLayout = ({ id }: { id: IDesignLayout['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/layout/layout.get.server.ts b/app/models/design/layout/layout.get.server.ts new file mode 100644 index 00000000..93e702d9 --- /dev/null +++ b/app/models/design/layout/layout.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignLayout } from './layout.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design layout.', + ) + } +} + +export const getDesignLayout = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.LAYOUT, + }, + }) + invariant(design, 'Design Layout Not found') + return deserializeDesign({ design }) as IDesignLayout +} diff --git a/app/models/design/layout/layout.server.ts b/app/models/design/layout/layout.server.ts new file mode 100644 index 00000000..0bd6365e --- /dev/null +++ b/app/models/design/layout/layout.server.ts @@ -0,0 +1,23 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignLayout extends IDesignParsed { + type: typeof DesignTypeEnum.LAYOUT + attributes: IDesignAttributesLayout +} + +export type IDesignLayoutStyle = 'random' | 'grid' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesLayout { + basis?: IDesignLayoutStyle + count?: number + rows?: number + columns?: number +} + +export interface IDesignLayoutSubmission + extends IDesignSubmission, + IDesignAttributesLayout {} diff --git a/app/models/design/layout/layout.update.server.ts b/app/models/design/layout/layout.update.server.ts new file mode 100644 index 00000000..cd127b19 --- /dev/null +++ b/app/models/design/layout/layout.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignLayoutSubmission, + type IDesignAttributesLayout, + type IDesignLayout, +} from './layout.server' + +export interface IDesignLayoutUpdatedResponse { + success: boolean + message?: string + updatedDesignLayout?: IDesign +} + +export interface IDesignLayoutUpdateSubmission extends IDesignLayoutSubmission { + id: IDesignLayout['id'] +} + +export interface IDesignLayoutUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesLayout +} diff --git a/app/models/design/layout/layout.update.style.server.ts b/app/models/design/layout/layout.update.style.server.ts new file mode 100644 index 00000000..6cedd95d --- /dev/null +++ b/app/models/design/layout/layout.update.style.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignLayoutStyleSchema } from '#app/schema/design/layout' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignLayoutStyle, + type IDesignAttributesLayout, + type IDesignLayout, +} from './layout.server' +import { stringifyDesignLayoutAttributes } from './utils' + +export const validateEditStyleDesignLayoutSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignLayoutStyleSchema, + strategy, + }) +} + +export interface IDesignLayoutUpdateStyleSubmission { + userId: IUser['id'] + id: IDesignLayout['id'] + style: IDesignLayoutStyle +} + +interface IDesignLayoutUpdateStyleData { + attributes: IDesignAttributesLayout +} + +export const updateDesignLayoutStyle = ({ + id, + data, +}: { + id: IDesignLayout['id'] + data: IDesignLayoutUpdateStyleData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignLayoutAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/layout/utils.ts b/app/models/design/layout/utils.ts new file mode 100644 index 00000000..bbb21eba --- /dev/null +++ b/app/models/design/layout/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesLayoutSchema } from '#app/schema/design/layout' +import { type IDesignAttributesLayout } from './layout.server' + +export const parseDesignLayoutAttributes = ( + attributes: string, +): IDesignAttributesLayout => { + try { + return DesignAttributesLayoutSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignLayoutAttributes = ( + attributes: IDesignAttributesLayout, +): string => { + try { + return JSON.stringify(DesignAttributesLayoutSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index 9f064440..8e8704fa 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -2,6 +2,7 @@ import { ZodError } from 'zod' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { type IDesign, type IDesignParsed } from './design.server' import { parseDesignFillAttributes } from './fill/utils' +import { parseDesignLayoutAttributes } from './layout/utils' import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ @@ -43,6 +44,8 @@ export const validateDesignAttributes = ({ switch (type) { case DesignTypeEnum.FILL: return parseDesignFillAttributes(attributes) + case DesignTypeEnum.LAYOUT: + return parseDesignLayoutAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) default: diff --git a/app/schema/design/layout.ts b/app/schema/design/layout.ts new file mode 100644 index 00000000..42aad80c --- /dev/null +++ b/app/schema/design/layout.ts @@ -0,0 +1,57 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +export const LayoutStyleTypeEnum = { + RANDOM: 'random', // place count of templates randomly in the container + NONE: 'none', // set rows and columns to place templates in a grid + // add more style types here, like 'spiral', 'circle', etc. ... ok copilot +} as const +export type layoutStyleTypeEnum = ObjectValues + +const LayoutStyleSchema = z.nativeEnum(LayoutStyleTypeEnum) +const LayoutCountSchema = z.number().min(1).max(100_000) +const LayoutGridSchema = z.number().min(1).max(3_000) + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesLayoutSchema = z.object({ + style: LayoutStyleSchema.optional(), + count: LayoutCountSchema.optional(), + rows: LayoutGridSchema.optional(), + columns: LayoutGridSchema.optional(), +}) + +export const NewDesignLayoutSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + style: LayoutStyleSchema.default(LayoutStyleTypeEnum.RANDOM), + count: LayoutCountSchema.default(1_000), + rows: LayoutGridSchema.default(9), + columns: LayoutGridSchema.default(9), +}) + +export const EditDesignLayoutStyleSchema = z.object({ + id: z.string(), + style: LayoutStyleSchema, +}) + +export const EditDesignLayoutCountSchema = z.object({ + id: z.string(), + count: LayoutCountSchema, +}) + +export const EditDesignLayoutRowsSchema = z.object({ + id: z.string(), + rows: LayoutGridSchema, +}) + +export const EditDesignLayoutColumnsSchema = z.object({ + id: z.string(), + columns: LayoutGridSchema, +}) + +export const DeleteDesignLayoutSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 0d7e7806..4496015d 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -3,9 +3,12 @@ import { type IDesignWithStroke, type IDesignWithFill, type IDesignWithType, + type IDesignWithLayout, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' +import { type IDesignAttributesLayout } from '#app/models/design/layout/layout.server' +import { stringifyDesignLayoutAttributes } from '#app/models/design/layout/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum } from '#app/schema/design' @@ -69,6 +72,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { switch (design.type) { case DesignTypeEnum.FILL: return updateDesignFillAttributes(design as IDesignWithFill) + case DesignTypeEnum.LAYOUT: + return updateDesignLayoutAttributes(design as IDesignWithLayout) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -105,6 +110,35 @@ const updateDesignFillAttributes = (design: IDesignWithFill) => { }) } +const updateDesignLayoutAttributes = (design: IDesignWithLayout) => { + const { layout } = design + const { style, count, rows, columns } = layout + const attributes = { + style, + count, + rows, + columns, + } as IDesignAttributesLayout + const jsonAttributes = stringifyDesignLayoutAttributes(attributes) + + return prisma.design + .update({ + where: { id: design.id }, + data: { + attributes: jsonAttributes, + }, + }) + .then(() => { + console.log(`Updated design layout attributes for design ${design.id}`) + }) + .catch(error => { + console.error( + `Failed to update design layout attributes for design ${design.id}`, + error, + ) + }) +} + const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { const { stroke } = design const { basis, style, value } = stroke From b0600387f51404d422005e5b9455d8bf4bad27c9 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 11:58:22 -0400 Subject: [PATCH 3/7] line, refactor script --- app/models/design/design.server.ts | 8 +- app/models/design/line/line.delete.server.ts | 13 ++ app/models/design/line/line.get.server.ts | 52 ++++++++ app/models/design/line/line.server.ts | 29 ++++ .../design/line/line.update.basis.server.ts | 53 ++++++++ app/models/design/line/line.update.server.ts | 20 +++ app/models/design/line/utils.ts | 39 ++++++ app/models/design/utils.ts | 3 + app/schema/design/line.ts | 59 +++++++++ .../populate-design-attributes-by-type.ts | 125 ++++++++++-------- 10 files changed, 345 insertions(+), 56 deletions(-) create mode 100644 app/models/design/line/line.delete.server.ts create mode 100644 app/models/design/line/line.get.server.ts create mode 100644 app/models/design/line/line.server.ts create mode 100644 app/models/design/line/line.update.basis.server.ts create mode 100644 app/models/design/line/line.update.server.ts create mode 100644 app/models/design/line/utils.ts create mode 100644 app/schema/design/line.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index f6eef028..9c84ab43 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -31,6 +31,10 @@ import { type IDesignLayout, type IDesignAttributesLayout, } from './layout/layout.server' +import { + type IDesignAttributesLine, + type IDesignLine, +} from './line/line.server' import { type IDesignStroke, type IDesignAttributesStroke, @@ -58,6 +62,7 @@ export interface IDesign extends BaseDesign { export type IDesignAttributes = | IDesignAttributesFill | IDesignAttributesLayout + | IDesignAttributesLine | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { @@ -71,7 +76,8 @@ export interface IDesignParsed extends BaseDesign { // TODO: replace with this ^^ export type IDesignByType = { designFills: IDesignFill[] - designLayoutss: IDesignLayout[] + designLayouts: IDesignLayout[] + designLines: IDesignLine[] designStroke: IDesignStroke[] } diff --git a/app/models/design/line/line.delete.server.ts b/app/models/design/line/line.delete.server.ts new file mode 100644 index 00000000..8f89f582 --- /dev/null +++ b/app/models/design/line/line.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignLine } from './line.server' + +export interface IDesignLineDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignLine = ({ id }: { id: IDesignLine['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/line/line.get.server.ts b/app/models/design/line/line.get.server.ts new file mode 100644 index 00000000..fec0b7ec --- /dev/null +++ b/app/models/design/line/line.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignLine } from './line.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design line.', + ) + } +} + +export const getDesignLine = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.LINE, + }, + }) + invariant(design, 'Design Line Not found') + return deserializeDesign({ design }) as IDesignLine +} diff --git a/app/models/design/line/line.server.ts b/app/models/design/line/line.server.ts new file mode 100644 index 00000000..ecaf000f --- /dev/null +++ b/app/models/design/line/line.server.ts @@ -0,0 +1,29 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignLine extends IDesignParsed { + type: typeof DesignTypeEnum.LINE + attributes: IDesignAttributesLine +} + +export type IDesignLineBasis = + | 'size' + | 'width' + | 'height' + | 'canvas-width' + | 'canvas-height' + +export type IDesignLineFormat = 'pixel' | 'percent' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesLine { + basis?: IDesignLineBasis + format?: IDesignLineFormat + width?: number +} + +export interface IDesignLineSubmission + extends IDesignSubmission, + IDesignAttributesLine {} diff --git a/app/models/design/line/line.update.basis.server.ts b/app/models/design/line/line.update.basis.server.ts new file mode 100644 index 00000000..8d4a7776 --- /dev/null +++ b/app/models/design/line/line.update.basis.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignLineBasisSchema } from '#app/schema/design/line' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignLineBasis, + type IDesignAttributesLine, + type IDesignLine, +} from './line.server' +import { stringifyDesignLineAttributes } from './utils' + +export const validateEditBasisDesignLineSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignLineBasisSchema, + strategy, + }) +} + +export interface IDesignLineUpdateBasisSubmission { + userId: IUser['id'] + id: IDesignLine['id'] + basis: IDesignLineBasis +} + +interface IDesignLineUpdateBasisData { + attributes: IDesignAttributesLine +} + +export const updateDesignLineBasis = ({ + id, + data, +}: { + id: IDesignLine['id'] + data: IDesignLineUpdateBasisData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignLineAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/line/line.update.server.ts b/app/models/design/line/line.update.server.ts new file mode 100644 index 00000000..48219c4d --- /dev/null +++ b/app/models/design/line/line.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignLineSubmission, + type IDesignAttributesLine, + type IDesignLine, +} from './line.server' + +export interface IDesignLineUpdatedResponse { + success: boolean + message?: string + updatedDesignLine?: IDesign +} + +export interface IDesignLineUpdateSubmission extends IDesignLineSubmission { + id: IDesignLine['id'] +} + +export interface IDesignLineUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesLine +} diff --git a/app/models/design/line/utils.ts b/app/models/design/line/utils.ts new file mode 100644 index 00000000..ed87539d --- /dev/null +++ b/app/models/design/line/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesLineSchema } from '#app/schema/design/line' +import { type IDesignAttributesLine } from './line.server' + +export const parseDesignLineAttributes = ( + attributes: string, +): IDesignAttributesLine => { + try { + return DesignAttributesLineSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignLineAttributes = ( + attributes: IDesignAttributesLine, +): string => { + try { + return JSON.stringify(DesignAttributesLineSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index 8e8704fa..5eba4c67 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -3,6 +3,7 @@ import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { type IDesign, type IDesignParsed } from './design.server' import { parseDesignFillAttributes } from './fill/utils' import { parseDesignLayoutAttributes } from './layout/utils' +import { parseDesignLineAttributes } from './line/utils' import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ @@ -46,6 +47,8 @@ export const validateDesignAttributes = ({ return parseDesignFillAttributes(attributes) case DesignTypeEnum.LAYOUT: return parseDesignLayoutAttributes(attributes) + case DesignTypeEnum.LINE: + return parseDesignLineAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) default: diff --git a/app/schema/design/line.ts b/app/schema/design/line.ts new file mode 100644 index 00000000..79bd027b --- /dev/null +++ b/app/schema/design/line.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +export const LineFormatTypeEnum = { + PIXEL: 'pixel', // exact pixel value + PERCENT: 'percent', // percent of basis length + // add more format types here +} as const +export const LineBasisTypeEnum = { + SIZE: 'size', + WIDTH: 'width', + HEIGHT: 'height', + CANVAS_WIDTH: 'canvas-width', + CANVAS_HEIGHT: 'canvas-height', + // add more styles here, like gradient, pattern, etc. +} as const +export type lineFormatTypeEnum = ObjectValues +export type lineBasisTypeEnum = ObjectValues + +const LineFormatSchema = z.nativeEnum(LineFormatTypeEnum) +const LineBasisSchema = z.nativeEnum(LineBasisTypeEnum) +const LineWidthSchema = z.number().positive() + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesLineSchema = z.object({ + basis: LineBasisSchema.optional(), + format: LineFormatSchema.optional(), + width: LineWidthSchema.optional(), +}) + +export const NewDesignLineSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + basis: LineBasisSchema.default(LineBasisTypeEnum.SIZE), + format: LineFormatSchema.default(LineFormatTypeEnum.PERCENT), + width: LineWidthSchema.default(3), +}) + +export const EditDesignLineBasisSchema = z.object({ + id: z.string(), + basis: LineBasisSchema, +}) + +export const EditDesignLineFormatSchema = z.object({ + id: z.string(), + format: LineFormatSchema, +}) + +export const EditDesignLineWidthSchema = z.object({ + id: z.string(), + width: LineWidthSchema, +}) + +export const DeleteDesignLineSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 4496015d..335b888e 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -4,14 +4,18 @@ import { type IDesignWithFill, type IDesignWithType, type IDesignWithLayout, + type IDesignWithLine, + type IDesign, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' import { type IDesignAttributesLayout } from '#app/models/design/layout/layout.server' import { stringifyDesignLayoutAttributes } from '#app/models/design/layout/utils' +import { type IDesignAttributesLine } from '#app/models/design/line/line.server' +import { stringifyDesignLineAttributes } from '#app/models/design/line/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' -import { DesignTypeEnum } from '#app/schema/design' +import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { prisma } from '#app/utils/db.server' // designs will have attributes string as json now @@ -74,6 +78,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignFillAttributes(design as IDesignWithFill) case DesignTypeEnum.LAYOUT: return updateDesignLayoutAttributes(design as IDesignWithLayout) + case DesignTypeEnum.LINE: + return updateDesignLineAttributes(design as IDesignWithLine) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -82,89 +88,98 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { } } -const updateDesignFillAttributes = (design: IDesignWithFill) => { - const { fill } = design - const { basis, style, value } = fill - const attributes = { - basis, - style, - value, - } as IDesignAttributesFill - const jsonAttributes = stringifyDesignFillAttributes(attributes) - +const prismaUpdatePromise = ({ + id, + type, + attributes, +}: { + id: IDesign['id'] + type: designTypeEnum + attributes: string +}) => { return prisma.design .update({ - where: { id: design.id }, - data: { - attributes: jsonAttributes, - }, + where: { id }, + data: { attributes }, }) .then(() => { - console.log(`Updated design fill attributes for design ${design.id}`) + console.log(`Updated design ${type} attributes for design ${id}`) }) .catch(error => { console.error( - `Failed to update design fill attributes for design ${design.id}`, + `Failed to update design ${type} attributes for design ${id}`, error, ) }) } +const updateDesignFillAttributes = (design: IDesignWithFill) => { + const { id, fill } = design + const { basis, style, value } = fill + const json = { + basis, + style, + value, + } as IDesignAttributesFill + const attributes = stringifyDesignFillAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.FILL, + attributes, + }) +} + const updateDesignLayoutAttributes = (design: IDesignWithLayout) => { - const { layout } = design + const { id, layout } = design const { style, count, rows, columns } = layout - const attributes = { + const json = { style, count, rows, columns, } as IDesignAttributesLayout - const jsonAttributes = stringifyDesignLayoutAttributes(attributes) + const attributes = stringifyDesignLayoutAttributes(json) - return prisma.design - .update({ - where: { id: design.id }, - data: { - attributes: jsonAttributes, - }, - }) - .then(() => { - console.log(`Updated design layout attributes for design ${design.id}`) - }) - .catch(error => { - console.error( - `Failed to update design layout attributes for design ${design.id}`, - error, - ) - }) + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.LAYOUT, + attributes, + }) +} + +const updateDesignLineAttributes = (design: IDesignWithLine) => { + const { id, line } = design + const { basis, format, width } = line + const json = { + basis, + format, + width, + } as IDesignAttributesLine + const attributes = stringifyDesignLineAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.LINE, + attributes, + }) } const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { - const { stroke } = design + const { id, stroke } = design const { basis, style, value } = stroke - const attributes = { + const json = { basis, style, value, } as IDesignAttributesStroke - const jsonAttributes = stringifyDesignStrokeAttributes(attributes) + const attributes = stringifyDesignStrokeAttributes(json) - return prisma.design - .update({ - where: { id: design.id }, - data: { - attributes: jsonAttributes, - }, - }) - .then(() => { - console.log(`Updated design stroke attributes for design ${design.id}`) - }) - .catch(error => { - console.error( - `Failed to update design stroke attributes for design ${design.id}`, - error, - ) - }) + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.STROKE, + attributes, + }) } await populateDesignAttributesByType() From 32ae57ee27cb6ac202d21a9eeb0bbb18a1b8a003 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 12:16:52 -0400 Subject: [PATCH 4/7] palette --- app/models/design/design.server.ts | 6 +++ .../design/palette/palette.delete.server.ts | 13 +++++ .../design/palette/palette.get.server.ts | 52 +++++++++++++++++++ app/models/design/palette/palette.server.ts | 18 +++++++ .../design/palette/palette.update.server.ts | 21 ++++++++ app/models/design/palette/utils.ts | 39 ++++++++++++++ app/models/design/utils.ts | 3 ++ app/schema/design/palette.ts | 27 ++++++++++ .../populate-design-attributes-by-type.ts | 20 +++++++ 9 files changed, 199 insertions(+) create mode 100644 app/models/design/palette/palette.delete.server.ts create mode 100644 app/models/design/palette/palette.get.server.ts create mode 100644 app/models/design/palette/palette.server.ts create mode 100644 app/models/design/palette/palette.update.server.ts create mode 100644 app/models/design/palette/utils.ts create mode 100644 app/schema/design/palette.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index 9c84ab43..e9820eb3 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -35,6 +35,10 @@ import { type IDesignAttributesLine, type IDesignLine, } from './line/line.server' +import { + type IDesignAttributesPalette, + type IDesignPalette, +} from './palette/palette.server' import { type IDesignStroke, type IDesignAttributesStroke, @@ -63,6 +67,7 @@ export type IDesignAttributes = | IDesignAttributesFill | IDesignAttributesLayout | IDesignAttributesLine + | IDesignAttributesPalette | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { @@ -78,6 +83,7 @@ export type IDesignByType = { designFills: IDesignFill[] designLayouts: IDesignLayout[] designLines: IDesignLine[] + designPalettes: IDesignPalette[] designStroke: IDesignStroke[] } diff --git a/app/models/design/palette/palette.delete.server.ts b/app/models/design/palette/palette.delete.server.ts new file mode 100644 index 00000000..7ddae355 --- /dev/null +++ b/app/models/design/palette/palette.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignPalette } from './palette.server' + +export interface IDesignPaletteDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignPalette = ({ id }: { id: IDesignPalette['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/palette/palette.get.server.ts b/app/models/design/palette/palette.get.server.ts new file mode 100644 index 00000000..9700ac93 --- /dev/null +++ b/app/models/design/palette/palette.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignPalette } from './palette.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design palette.', + ) + } +} + +export const getDesignPalette = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.PALETTE, + }, + }) + invariant(design, 'Design Palette Not found') + return deserializeDesign({ design }) as IDesignPalette +} diff --git a/app/models/design/palette/palette.server.ts b/app/models/design/palette/palette.server.ts new file mode 100644 index 00000000..715dca0b --- /dev/null +++ b/app/models/design/palette/palette.server.ts @@ -0,0 +1,18 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignPalette extends IDesignParsed { + type: typeof DesignTypeEnum.PALETTE + attributes: IDesignAttributesPalette +} + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesPalette { + value?: string +} + +export interface IDesignPaletteSubmission + extends IDesignSubmission, + IDesignAttributesPalette {} diff --git a/app/models/design/palette/palette.update.server.ts b/app/models/design/palette/palette.update.server.ts new file mode 100644 index 00000000..06959333 --- /dev/null +++ b/app/models/design/palette/palette.update.server.ts @@ -0,0 +1,21 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignPaletteSubmission, + type IDesignAttributesPalette, + type IDesignPalette, +} from './palette.server' + +export interface IDesignPaletteUpdatedResponse { + success: boolean + message?: string + updatedDesignPalette?: IDesign +} + +export interface IDesignPaletteUpdateSubmission + extends IDesignPaletteSubmission { + id: IDesignPalette['id'] +} + +export interface IDesignPaletteUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesPalette +} diff --git a/app/models/design/palette/utils.ts b/app/models/design/palette/utils.ts new file mode 100644 index 00000000..5a07e7d0 --- /dev/null +++ b/app/models/design/palette/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesPaletteSchema } from '#app/schema/design/palette' +import { type IDesignAttributesPalette } from './palette.server' + +export const parseDesignPaletteAttributes = ( + attributes: string, +): IDesignAttributesPalette => { + try { + return DesignAttributesPaletteSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignPaletteAttributes = ( + attributes: IDesignAttributesPalette, +): string => { + try { + return JSON.stringify(DesignAttributesPaletteSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index 5eba4c67..219cc7e4 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -4,6 +4,7 @@ import { type IDesign, type IDesignParsed } from './design.server' import { parseDesignFillAttributes } from './fill/utils' import { parseDesignLayoutAttributes } from './layout/utils' import { parseDesignLineAttributes } from './line/utils' +import { parseDesignPaletteAttributes } from './palette/utils' import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ @@ -49,6 +50,8 @@ export const validateDesignAttributes = ({ return parseDesignLayoutAttributes(attributes) case DesignTypeEnum.LINE: return parseDesignLineAttributes(attributes) + case DesignTypeEnum.PALETTE: + return parseDesignPaletteAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) default: diff --git a/app/schema/design/palette.ts b/app/schema/design/palette.ts new file mode 100644 index 00000000..08ea6ff4 --- /dev/null +++ b/app/schema/design/palette.ts @@ -0,0 +1,27 @@ +import { z } from 'zod' +import { HexcodeSchema } from '../colors' + +const PaletteValueSchema = HexcodeSchema + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesPaletteSchema = z.object({ + value: PaletteValueSchema.optional(), +}) + +export const NewDesignPaletteSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + value: PaletteValueSchema.default('000000'), +}) + +export const EditDesignPaletteValueSchema = z.object({ + id: z.string(), + width: PaletteValueSchema, +}) + +export const DeleteDesignPaletteSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 335b888e..825c7ed5 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -6,6 +6,7 @@ import { type IDesignWithLayout, type IDesignWithLine, type IDesign, + type IDesignWithPalette, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' @@ -13,6 +14,8 @@ import { type IDesignAttributesLayout } from '#app/models/design/layout/layout.s import { stringifyDesignLayoutAttributes } from '#app/models/design/layout/utils' import { type IDesignAttributesLine } from '#app/models/design/line/line.server' import { stringifyDesignLineAttributes } from '#app/models/design/line/utils' +import { type IDesignAttributesPalette } from '#app/models/design/palette/palette.server' +import { stringifyDesignPaletteAttributes } from '#app/models/design/palette/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' @@ -80,6 +83,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignLayoutAttributes(design as IDesignWithLayout) case DesignTypeEnum.LINE: return updateDesignLineAttributes(design as IDesignWithLine) + case DesignTypeEnum.PALETTE: + return updateDesignPaletteAttributes(design as IDesignWithPalette) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -165,6 +170,21 @@ const updateDesignLineAttributes = (design: IDesignWithLine) => { }) } +const updateDesignPaletteAttributes = (design: IDesignWithPalette) => { + const { id, palette } = design + const { value } = palette + const json = { + value, + } as IDesignAttributesPalette + const attributes = stringifyDesignPaletteAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.PALETTE, + attributes, + }) +} + const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { const { id, stroke } = design const { basis, style, value } = stroke From 63bf53c5ffff353f64d7604cd4d4362a439f90dd Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 12:30:47 -0400 Subject: [PATCH 5/7] rotate --- app/models/design/design.server.ts | 6 ++ .../design/rotate/rotate.delete.server.ts | 13 ++++ app/models/design/rotate/rotate.get.server.ts | 52 ++++++++++++++ app/models/design/rotate/rotate.server.ts | 42 ++++++++++++ .../rotate/rotate.update.basis.server.ts | 53 +++++++++++++++ .../design/rotate/rotate.update.server.ts | 20 ++++++ app/models/design/rotate/utils.ts | 39 +++++++++++ app/models/design/utils.ts | 3 + app/schema/design/rotate.ts | 67 +++++++++++++++++++ .../populate-design-attributes-by-type.ts | 21 ++++++ 10 files changed, 316 insertions(+) create mode 100644 app/models/design/rotate/rotate.delete.server.ts create mode 100644 app/models/design/rotate/rotate.get.server.ts create mode 100644 app/models/design/rotate/rotate.server.ts create mode 100644 app/models/design/rotate/rotate.update.basis.server.ts create mode 100644 app/models/design/rotate/rotate.update.server.ts create mode 100644 app/models/design/rotate/utils.ts create mode 100644 app/schema/design/rotate.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index e9820eb3..a6d472e5 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -39,6 +39,10 @@ import { type IDesignAttributesPalette, type IDesignPalette, } from './palette/palette.server' +import { + type IDesignAttributesRotate, + type IDesignRotate, +} from './rotate/rotate.server' import { type IDesignStroke, type IDesignAttributesStroke, @@ -68,6 +72,7 @@ export type IDesignAttributes = | IDesignAttributesLayout | IDesignAttributesLine | IDesignAttributesPalette + | IDesignAttributesRotate | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { @@ -84,6 +89,7 @@ export type IDesignByType = { designLayouts: IDesignLayout[] designLines: IDesignLine[] designPalettes: IDesignPalette[] + designRotates: IDesignRotate[] designStroke: IDesignStroke[] } diff --git a/app/models/design/rotate/rotate.delete.server.ts b/app/models/design/rotate/rotate.delete.server.ts new file mode 100644 index 00000000..9ff2458f --- /dev/null +++ b/app/models/design/rotate/rotate.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignRotate } from './rotate.server' + +export interface IDesignRotateDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignRotate = ({ id }: { id: IDesignRotate['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/rotate/rotate.get.server.ts b/app/models/design/rotate/rotate.get.server.ts new file mode 100644 index 00000000..a4190b63 --- /dev/null +++ b/app/models/design/rotate/rotate.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignRotate } from './rotate.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design line.', + ) + } +} + +export const getDesignRotate = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.ROTATE, + }, + }) + invariant(design, 'Design Rotate Not found') + return deserializeDesign({ design }) as IDesignRotate +} diff --git a/app/models/design/rotate/rotate.server.ts b/app/models/design/rotate/rotate.server.ts new file mode 100644 index 00000000..97014b34 --- /dev/null +++ b/app/models/design/rotate/rotate.server.ts @@ -0,0 +1,42 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignRotate extends IDesignParsed { + type: typeof DesignTypeEnum.ROTATE + attributes: IDesignAttributesRotate +} + +export type IDesignRotateArrayBasis = + | 'visible-random' + | 'visible-loop' + | 'visible-loop-reverse' + +export type IDesignRotateIndividualBasis = + | 'defined' + | 'random' + | 'N' + | 'NE' + | 'E' + | 'SE' + | 'S' + | 'SW' + | 'W' + | 'NW' + +export type IDesignRotateBasis = + | IDesignRotateIndividualBasis + | IDesignRotateArrayBasis + +export type IDesignRotateFormat = 'pixel' | 'percent' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesRotate { + basis?: IDesignRotateBasis + value?: number +} + +export interface IDesignRotateSubmission + extends IDesignSubmission, + IDesignAttributesRotate {} diff --git a/app/models/design/rotate/rotate.update.basis.server.ts b/app/models/design/rotate/rotate.update.basis.server.ts new file mode 100644 index 00000000..afd75bf2 --- /dev/null +++ b/app/models/design/rotate/rotate.update.basis.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignRotateBasisSchema } from '#app/schema/design/rotate' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignRotateBasis, + type IDesignAttributesRotate, + type IDesignRotate, +} from './rotate.server' +import { stringifyDesignRotateAttributes } from './utils' + +export const validateEditBasisDesignRotateSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignRotateBasisSchema, + strategy, + }) +} + +export interface IDesignRotateUpdateBasisSubmission { + userId: IUser['id'] + id: IDesignRotate['id'] + basis: IDesignRotateBasis +} + +interface IDesignRotateUpdateBasisData { + attributes: IDesignAttributesRotate +} + +export const updateDesignRotateBasis = ({ + id, + data, +}: { + id: IDesignRotate['id'] + data: IDesignRotateUpdateBasisData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignRotateAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/rotate/rotate.update.server.ts b/app/models/design/rotate/rotate.update.server.ts new file mode 100644 index 00000000..febd5da6 --- /dev/null +++ b/app/models/design/rotate/rotate.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignRotateSubmission, + type IDesignAttributesRotate, + type IDesignRotate, +} from './rotate.server' + +export interface IDesignRotateUpdatedResponse { + success: boolean + message?: string + updatedDesignRotate?: IDesign +} + +export interface IDesignRotateUpdateSubmission extends IDesignRotateSubmission { + id: IDesignRotate['id'] +} + +export interface IDesignRotateUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesRotate +} diff --git a/app/models/design/rotate/utils.ts b/app/models/design/rotate/utils.ts new file mode 100644 index 00000000..657f8c28 --- /dev/null +++ b/app/models/design/rotate/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesRotateSchema } from '#app/schema/design/rotate' +import { type IDesignAttributesRotate } from './rotate.server' + +export const parseDesignRotateAttributes = ( + attributes: string, +): IDesignAttributesRotate => { + try { + return DesignAttributesRotateSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignRotateAttributes = ( + attributes: IDesignAttributesRotate, +): string => { + try { + return JSON.stringify(DesignAttributesRotateSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index 219cc7e4..e615038b 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -5,6 +5,7 @@ import { parseDesignFillAttributes } from './fill/utils' import { parseDesignLayoutAttributes } from './layout/utils' import { parseDesignLineAttributes } from './line/utils' import { parseDesignPaletteAttributes } from './palette/utils' +import { parseDesignRotateAttributes } from './rotate/utils' import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ @@ -52,6 +53,8 @@ export const validateDesignAttributes = ({ return parseDesignLineAttributes(attributes) case DesignTypeEnum.PALETTE: return parseDesignPaletteAttributes(attributes) + case DesignTypeEnum.ROTATE: + return parseDesignRotateAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) default: diff --git a/app/schema/design/rotate.ts b/app/schema/design/rotate.ts new file mode 100644 index 00000000..a606e3eb --- /dev/null +++ b/app/schema/design/rotate.ts @@ -0,0 +1,67 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +// use this for determining if build should iterate through rotates array +export const RotateArrayBasisTypeEnum = { + VISIBLE_RANDOM: 'visible-random', + VISIBLE_LOOP: 'visible-loop', + VISIBLE_LOOP_REVERSE: 'visible-loop-reverse', +} as const +export type rotateArrayBasisTypeEnum = ObjectValues< + typeof RotateArrayBasisTypeEnum +> + +export const RotateIndividualBasisTypeEnum = { + DEFINED: 'defined', // exact rotation value + RANDOM: 'random', // random rotation value + N: 'N', // 0 degrees + NE: 'NE', // 45 degrees + E: 'E', // 90 degrees + SE: 'SE', // 135 degrees + S: 'S', // 180 degrees + SW: 'SW', // 225 degrees + W: 'W', // 270 degrees + NW: 'NW', // 315 degrees +} as const +export type rotateIndividualBasisTypeEnum = ObjectValues< + typeof RotateIndividualBasisTypeEnum +> + +export const RotateBasisTypeEnum = { + ...RotateIndividualBasisTypeEnum, + ...RotateArrayBasisTypeEnum, + // add more basis types here +} as const +export type rotateBasisTypeEnum = ObjectValues + +const RotateBasisSchema = z.nativeEnum(RotateBasisTypeEnum) + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesRotateSchema = z.object({ + basis: RotateBasisSchema.optional(), + value: z.number().optional(), +}) + +export const NewDesignRotateSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + basis: RotateBasisSchema.default(RotateBasisTypeEnum.DEFINED), + value: z.number().default(0), +}) + +export const EditDesignRotateBasisSchema = z.object({ + id: z.string(), + basis: RotateBasisSchema, +}) + +export const EditDesignRotateValueSchema = z.object({ + id: z.string(), + value: z.number(), +}) + +export const DeleteDesignRotateSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 825c7ed5..e1d8ce51 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -7,6 +7,7 @@ import { type IDesignWithLine, type IDesign, type IDesignWithPalette, + type IDesignWithRotate, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' @@ -16,6 +17,8 @@ import { type IDesignAttributesLine } from '#app/models/design/line/line.server' import { stringifyDesignLineAttributes } from '#app/models/design/line/utils' import { type IDesignAttributesPalette } from '#app/models/design/palette/palette.server' import { stringifyDesignPaletteAttributes } from '#app/models/design/palette/utils' +import { type IDesignAttributesRotate } from '#app/models/design/rotate/rotate.server' +import { stringifyDesignRotateAttributes } from '#app/models/design/rotate/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' @@ -85,6 +88,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignLineAttributes(design as IDesignWithLine) case DesignTypeEnum.PALETTE: return updateDesignPaletteAttributes(design as IDesignWithPalette) + case DesignTypeEnum.ROTATE: + return updateDesignRotateAttributes(design as IDesignWithRotate) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -185,6 +190,22 @@ const updateDesignPaletteAttributes = (design: IDesignWithPalette) => { }) } +const updateDesignRotateAttributes = (design: IDesignWithRotate) => { + const { id, rotate } = design + const { basis, value } = rotate + const json = { + basis, + value, + } as IDesignAttributesRotate + const attributes = stringifyDesignRotateAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.ROTATE, + attributes, + }) +} + const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { const { id, stroke } = design const { basis, style, value } = stroke From c4312da439db70d4f848d5f2c4609d223cfb2bee Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 12:59:24 -0400 Subject: [PATCH 6/7] size --- app/models/design/size/size.delete.server.ts | 13 ++++ app/models/design/size/size.get.server.ts | 52 ++++++++++++++++ app/models/design/size/size.server.ts | 28 +++++++++ .../design/size/size.update.basis.server.ts | 53 +++++++++++++++++ app/models/design/size/size.update.server.ts | 20 +++++++ app/models/design/size/utils.ts | 39 ++++++++++++ app/schema/design/size.ts | 59 +++++++++++++++++++ .../populate-design-attributes-by-type.ts | 22 +++++++ 8 files changed, 286 insertions(+) create mode 100644 app/models/design/size/size.delete.server.ts create mode 100644 app/models/design/size/size.get.server.ts create mode 100644 app/models/design/size/size.server.ts create mode 100644 app/models/design/size/size.update.basis.server.ts create mode 100644 app/models/design/size/size.update.server.ts create mode 100644 app/models/design/size/utils.ts create mode 100644 app/schema/design/size.ts diff --git a/app/models/design/size/size.delete.server.ts b/app/models/design/size/size.delete.server.ts new file mode 100644 index 00000000..c6b0e974 --- /dev/null +++ b/app/models/design/size/size.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignSize } from './size.server' + +export interface IDesignSizeDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignSize = ({ id }: { id: IDesignSize['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/size/size.get.server.ts b/app/models/design/size/size.get.server.ts new file mode 100644 index 00000000..de64b466 --- /dev/null +++ b/app/models/design/size/size.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignSize } from './size.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design size.', + ) + } +} + +export const getDesignSize = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.SIZE, + }, + }) + invariant(design, 'Design Size Not found') + return deserializeDesign({ design }) as IDesignSize +} diff --git a/app/models/design/size/size.server.ts b/app/models/design/size/size.server.ts new file mode 100644 index 00000000..ec73bef0 --- /dev/null +++ b/app/models/design/size/size.server.ts @@ -0,0 +1,28 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignSize extends IDesignParsed { + type: typeof DesignTypeEnum.SIZE + attributes: IDesignAttributesSize +} + +export type IDesignSizeBasis = + | 'width' + | 'height' + | 'canvas-width' + | 'canvas-height' + +export type IDesignSizeFormat = 'pixel' | 'percent' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesSize { + basis?: IDesignSizeBasis + format?: IDesignSizeFormat + value?: number +} + +export interface IDesignSizeSubmission + extends IDesignSubmission, + IDesignAttributesSize {} diff --git a/app/models/design/size/size.update.basis.server.ts b/app/models/design/size/size.update.basis.server.ts new file mode 100644 index 00000000..0decacd1 --- /dev/null +++ b/app/models/design/size/size.update.basis.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignSizeBasisSchema } from '#app/schema/design/size' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignSizeBasis, + type IDesignAttributesSize, + type IDesignSize, +} from './size.server' +import { stringifyDesignSizeAttributes } from './utils' + +export const validateEditBasisDesignSizeSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignSizeBasisSchema, + strategy, + }) +} + +export interface IDesignSizeUpdateBasisSubmission { + userId: IUser['id'] + id: IDesignSize['id'] + basis: IDesignSizeBasis +} + +interface IDesignSizeUpdateBasisData { + attributes: IDesignAttributesSize +} + +export const updateDesignSizeBasis = ({ + id, + data, +}: { + id: IDesignSize['id'] + data: IDesignSizeUpdateBasisData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignSizeAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/size/size.update.server.ts b/app/models/design/size/size.update.server.ts new file mode 100644 index 00000000..a07c4f32 --- /dev/null +++ b/app/models/design/size/size.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignSizeSubmission, + type IDesignAttributesSize, + type IDesignSize, +} from './size.server' + +export interface IDesignSizeUpdatedResponse { + success: boolean + message?: string + updatedDesignSize?: IDesign +} + +export interface IDesignSizeUpdateSubmission extends IDesignSizeSubmission { + id: IDesignSize['id'] +} + +export interface IDesignSizeUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesSize +} diff --git a/app/models/design/size/utils.ts b/app/models/design/size/utils.ts new file mode 100644 index 00000000..be06aa4d --- /dev/null +++ b/app/models/design/size/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesSizeSchema } from '#app/schema/design/size' +import { type IDesignAttributesSize } from './size.server' + +export const parseDesignSizeAttributes = ( + attributes: string, +): IDesignAttributesSize => { + try { + return DesignAttributesSizeSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignSizeAttributes = ( + attributes: IDesignAttributesSize, +): string => { + try { + return JSON.stringify(DesignAttributesSizeSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/schema/design/size.ts b/app/schema/design/size.ts new file mode 100644 index 00000000..a0a7b068 --- /dev/null +++ b/app/schema/design/size.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +export const SizeFormatTypeEnum = { + PIXEL: 'pixel', // exact pixel value + PERCENT: 'percent', // percent of basis length + // add more format types here +} as const +export const SizeBasisTypeEnum = { + WIDTH: 'width', + HEIGHT: 'height', + CANVAS_WIDTH: 'canvas-width', + CANVAS_HEIGHT: 'canvas-height', + // add more styles here, like gradient, pattern, etc. +} as const + +export type sizeFormatTypeEnum = ObjectValues +export type sizeBasisTypeEnum = ObjectValues + +const SizeFormatSchema = z.nativeEnum(SizeFormatTypeEnum) +const SizeBasisSchema = z.nativeEnum(SizeBasisTypeEnum) +const SizeValueSchema = z.number().positive() + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesSizeSchema = z.object({ + basis: SizeBasisSchema.optional(), + format: SizeFormatSchema.optional(), + value: SizeValueSchema.optional(), +}) + +export const NewDesignSizeSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + basis: SizeBasisSchema.default(SizeBasisTypeEnum.WIDTH), + format: SizeFormatSchema.default(SizeFormatTypeEnum.PERCENT), + value: SizeValueSchema.default(10), +}) + +export const EditDesignSizeBasisSchema = z.object({ + id: z.string(), + basis: SizeBasisSchema, +}) + +export const EditDesignSizeFormatSchema = z.object({ + id: z.string(), + format: SizeFormatSchema, +}) + +export const EditDesignSizeValueSchema = z.object({ + id: z.string(), + value: SizeValueSchema, +}) + +export const DeleteDesignSizeSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index e1d8ce51..f79f74e6 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -8,6 +8,7 @@ import { type IDesign, type IDesignWithPalette, type IDesignWithRotate, + type IDesignWithSize, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' @@ -19,6 +20,8 @@ import { type IDesignAttributesPalette } from '#app/models/design/palette/palett import { stringifyDesignPaletteAttributes } from '#app/models/design/palette/utils' import { type IDesignAttributesRotate } from '#app/models/design/rotate/rotate.server' import { stringifyDesignRotateAttributes } from '#app/models/design/rotate/utils' +import { type IDesignAttributesSize } from '#app/models/design/size/size.server' +import { stringifyDesignSizeAttributes } from '#app/models/design/size/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' @@ -90,6 +93,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignPaletteAttributes(design as IDesignWithPalette) case DesignTypeEnum.ROTATE: return updateDesignRotateAttributes(design as IDesignWithRotate) + case DesignTypeEnum.SIZE: + return updateDesignSizeAttributes(design as IDesignWithSize) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -206,6 +211,23 @@ const updateDesignRotateAttributes = (design: IDesignWithRotate) => { }) } +const updateDesignSizeAttributes = (design: IDesignWithSize) => { + const { id, size } = design + const { basis, format, value } = size + const json = { + basis, + format, + value, + } as IDesignAttributesSize + const attributes = stringifyDesignSizeAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.SIZE, + attributes, + }) +} + const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { const { id, stroke } = design const { basis, style, value } = stroke From 01da27646756ff26c04bc377bca5ee6604b5d822 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 13:10:29 -0400 Subject: [PATCH 7/7] template --- app/models/design/design.server.ts | 12 +++++ .../design/template/template.delete.server.ts | 13 +++++ .../design/template/template.get.server.ts | 52 ++++++++++++++++++ app/models/design/template/template.server.ts | 20 +++++++ .../design/template/template.update.server.ts | 21 ++++++++ .../template/template.update.style.server.ts | 53 +++++++++++++++++++ app/models/design/template/utils.ts | 39 ++++++++++++++ app/models/design/utils.ts | 6 +++ app/schema/design/template.ts | 32 +++++++++++ .../populate-design-attributes-by-type.ts | 20 +++++++ 10 files changed, 268 insertions(+) create mode 100644 app/models/design/template/template.delete.server.ts create mode 100644 app/models/design/template/template.get.server.ts create mode 100644 app/models/design/template/template.server.ts create mode 100644 app/models/design/template/template.update.server.ts create mode 100644 app/models/design/template/template.update.style.server.ts create mode 100644 app/models/design/template/utils.ts create mode 100644 app/schema/design/template.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index a6d472e5..45cb7a9f 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -43,10 +43,18 @@ import { type IDesignAttributesRotate, type IDesignRotate, } from './rotate/rotate.server' +import { + type IDesignAttributesSize, + type IDesignSize, +} from './size/size.server' import { type IDesignStroke, type IDesignAttributesStroke, } from './stroke/stroke.server' +import { + type IDesignAttributesTemplate, + type IDesignTemplate, +} from './template/template.server' // Omitting 'createdAt' and 'updatedAt' from the Design interface // prisma query returns a string for these fields @@ -73,7 +81,9 @@ export type IDesignAttributes = | IDesignAttributesLine | IDesignAttributesPalette | IDesignAttributesRotate + | IDesignAttributesSize | IDesignAttributesStroke + | IDesignAttributesTemplate export interface IDesignParsed extends BaseDesign { type: designTypeEnum @@ -90,7 +100,9 @@ export type IDesignByType = { designLines: IDesignLine[] designPalettes: IDesignPalette[] designRotates: IDesignRotate[] + designSizes: IDesignSize[] designStroke: IDesignStroke[] + designTemplates: IDesignTemplate[] } // export interface IDesignsByTypeWithType { diff --git a/app/models/design/template/template.delete.server.ts b/app/models/design/template/template.delete.server.ts new file mode 100644 index 00000000..0a12e8a7 --- /dev/null +++ b/app/models/design/template/template.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignTemplate } from './template.server' + +export interface IDesignTemplateDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignTemplate = ({ id }: { id: IDesignTemplate['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/template/template.get.server.ts b/app/models/design/template/template.get.server.ts new file mode 100644 index 00000000..1b60e5d8 --- /dev/null +++ b/app/models/design/template/template.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignTemplate } from './template.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design template.', + ) + } +} + +export const getDesignTemplate = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.TEMPLATE, + }, + }) + invariant(design, 'Design Template Not found') + return deserializeDesign({ design }) as IDesignTemplate +} diff --git a/app/models/design/template/template.server.ts b/app/models/design/template/template.server.ts new file mode 100644 index 00000000..6c9f3e12 --- /dev/null +++ b/app/models/design/template/template.server.ts @@ -0,0 +1,20 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignTemplate extends IDesignParsed { + type: typeof DesignTypeEnum.TEMPLATE + attributes: IDesignAttributesTemplate +} + +export type IDesignTemplateStyle = 'triangle' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesTemplate { + style?: IDesignTemplateStyle +} + +export interface IDesignTemplateSubmission + extends IDesignSubmission, + IDesignAttributesTemplate {} diff --git a/app/models/design/template/template.update.server.ts b/app/models/design/template/template.update.server.ts new file mode 100644 index 00000000..7324a0dd --- /dev/null +++ b/app/models/design/template/template.update.server.ts @@ -0,0 +1,21 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignTemplateSubmission, + type IDesignAttributesTemplate, + type IDesignTemplate, +} from './template.server' + +export interface IDesignTemplateUpdatedResponse { + success: boolean + message?: string + updatedDesignTemplate?: IDesign +} + +export interface IDesignTemplateUpdateSubmission + extends IDesignTemplateSubmission { + id: IDesignTemplate['id'] +} + +export interface IDesignTemplateUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesTemplate +} diff --git a/app/models/design/template/template.update.style.server.ts b/app/models/design/template/template.update.style.server.ts new file mode 100644 index 00000000..6a96ad53 --- /dev/null +++ b/app/models/design/template/template.update.style.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignTemplateStyleSchema } from '#app/schema/design/template' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignAttributesTemplate, + type IDesignTemplate, + type IDesignTemplateStyle, +} from './template.server' +import { stringifyDesignTemplateAttributes } from './utils' + +export const validateEditStyleDesignTemplateSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignTemplateStyleSchema, + strategy, + }) +} + +export interface IDesignTemplateUpdateStyleSubmission { + userId: IUser['id'] + id: IDesignTemplate['id'] + style: IDesignTemplateStyle +} + +interface IDesignTemplateUpdateStyleData { + attributes: IDesignAttributesTemplate +} + +export const updateDesignTemplateStyle = ({ + id, + data, +}: { + id: IDesignTemplate['id'] + data: IDesignTemplateUpdateStyleData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignTemplateAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/template/utils.ts b/app/models/design/template/utils.ts new file mode 100644 index 00000000..3fa5be17 --- /dev/null +++ b/app/models/design/template/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesTemplateSchema } from '#app/schema/design/template' +import { type IDesignAttributesTemplate } from './template.server' + +export const parseDesignTemplateAttributes = ( + attributes: string, +): IDesignAttributesTemplate => { + try { + return DesignAttributesTemplateSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignTemplateAttributes = ( + attributes: IDesignAttributesTemplate, +): string => { + try { + return JSON.stringify(DesignAttributesTemplateSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index e615038b..8d2b7bed 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -6,7 +6,9 @@ import { parseDesignLayoutAttributes } from './layout/utils' import { parseDesignLineAttributes } from './line/utils' import { parseDesignPaletteAttributes } from './palette/utils' import { parseDesignRotateAttributes } from './rotate/utils' +import { parseDesignSizeAttributes } from './size/utils' import { parseDesignStrokeAttributes } from './stroke/utils' +import { parseDesignTemplateAttributes } from './template/utils' export const deserializeDesigns = ({ designs, @@ -55,8 +57,12 @@ export const validateDesignAttributes = ({ return parseDesignPaletteAttributes(attributes) case DesignTypeEnum.ROTATE: return parseDesignRotateAttributes(attributes) + case DesignTypeEnum.SIZE: + return parseDesignSizeAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) + case DesignTypeEnum.TEMPLATE: + return parseDesignTemplateAttributes(attributes) default: throw new Error(`Unsupported design type: ${type}`) } diff --git a/app/schema/design/template.ts b/app/schema/design/template.ts new file mode 100644 index 00000000..8e65bca9 --- /dev/null +++ b/app/schema/design/template.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +export const TemplateStyleTypeEnum = { + TRIANGLE: 'triangle', // my only shape so far + // add more styles here +} as const +export type templateStyleTypeEnum = ObjectValues +const TemplateStyleSchema = z.nativeEnum(TemplateStyleTypeEnum) + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesTemplateSchema = z.object({ + style: TemplateStyleSchema.optional(), +}) + +export const NewDesignTemplateSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + style: TemplateStyleSchema.default(TemplateStyleTypeEnum.TRIANGLE), +}) + +export const EditDesignTemplateStyleSchema = z.object({ + id: z.string(), + style: TemplateStyleSchema, +}) + +export const DeleteDesignTemplateSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index f79f74e6..640901fe 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -9,6 +9,7 @@ import { type IDesignWithPalette, type IDesignWithRotate, type IDesignWithSize, + type IDesignWithTemplate, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' @@ -24,6 +25,8 @@ import { type IDesignAttributesSize } from '#app/models/design/size/size.server' import { stringifyDesignSizeAttributes } from '#app/models/design/size/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' +import { type IDesignAttributesTemplate } from '#app/models/design/template/template.server' +import { stringifyDesignTemplateAttributes } from '#app/models/design/template/utils' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { prisma } from '#app/utils/db.server' @@ -97,6 +100,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignSizeAttributes(design as IDesignWithSize) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) + case DesignTypeEnum.TEMPLATE: + return updateDesignTemplateAttributes(design as IDesignWithTemplate) default: return Promise.resolve() // throw new Error(`Unsupported design type: ${design.type}`) @@ -245,4 +250,19 @@ const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { }) } +const updateDesignTemplateAttributes = (design: IDesignWithTemplate) => { + const { id, template } = design + const { style } = template + const json = { + style, + } as IDesignAttributesTemplate + const attributes = stringifyDesignTemplateAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.TEMPLATE, + attributes, + }) +} + await populateDesignAttributesByType()