goodeats / epic-pppaaattt.xyz

A generative art generator 🚧 in active development
https://pppaaattt.xyz/
0 stars 0 forks source link

assets polymorphic model #174

Closed goodeats closed 4 months ago

goodeats commented 4 months ago

Background

A while back I moved designs to a polymorphic parent-child model architecture based on type Issue: https://github.com/goodeats/epic-pppaaattt.xyz/issues/28

It followed the advice of this article https://www.basedash.com/blog/how-to-model-inheritance-in-prisma

I was having trouble managing the JSON parse and stringify along with zod schema checks becoming a challenge to manage complexity in a scalable way

Problem

Discussion

Advantages of Using JSON Strings in the Design/Asset Model

  1. Simplified Schema: Reduces the number of tables, making the schema easier to manage and understand.
  2. Flexibility: Allows for easy addition of new design/asset types without altering the database schema.
  3. Efficiency: Fewer joins required, which can improve performance, especially in a relational database like SQLite.

1 is good 2 is very important, this project has gone from a fun side-project to something I could build for real users and I want to be able to meed their needs quickly 3 is fine, not as important until I need to scale... I'll be looking for help when that happens :D

Considerations and Trade-offs

  1. Data Integrity: Storing configurations as JSON strings moves validation to the application layer, so you need to ensure that the JSON structures are properly validated and parsed.
  2. Query Complexity: While simpler, querying specific elements within the JSON string will require parsing within your application logic, potentially increasing complexity in certain operations.
  3. Performance: Parsing JSON strings can be more computationally intensive than direct column access, but this is usually negligible unless dealing with very large datasets or high query volumes.

1 will have to be extremely diligent with using zod to validate schema on parse and stringify for each type 2 JSON string shouldn't have to be queried for anything; just have the data and the configuration details 3 see 3 from above :D

Implementation

Define Asset Types Enum: Create an enumerator for different asset types.

export const AssetTypeEnum = {
  IMAGE: 'image',
  PATTERN: 'pattern',
  GRADIENT: 'gradient',
  FILTER: 'filter',
  MASK: 'mask',
  COLOR_PALETTE: 'color_palette',
  SHAPE: 'shape',
  TEXT_STYLE: 'text_style',
  VECTOR: 'vector',
  BRUSH: 'brush',
  TEMPLATE: 'template',
  ANIMATION: 'animation',
} as const;

export type AssetTypeEnum = typeof AssetTypeEnum[keyof typeof AssetTypeEnum];

Define JSON Schemas for Each Asset Type

import { z } from 'zod';

// Example schema for an image asset
const ImageAssetSchema = z.object({
  altText: z.string().optional(),
  contentType: z.string(),
  blob: z.string(), // Assuming the image data is stored as a base64 encoded string
});

// Example schema for a pattern asset
const PatternAssetSchema = z.object({
  name: z.string(),
  description: z.string().optional(),
  blob: z.string(), // Assuming the pattern data is stored as a base64 encoded string
});

// Example schema for a gradient asset
const GradientAssetSchema = z.object({
  name: z.string(),
  description: z.string().optional(),
  stops: z.array(z.object({
    offset: z.number(),
    color: z.string(),
    opacity: z.number().min(0).max(1),
  })),
});

// Define other schemas similarly...

// Schema for all asset types
const AssetDataSchema = z.union([
  ImageAssetSchema,
  PatternAssetSchema,
  GradientAssetSchema,
  // Add other schemas here...
]);

Modify the Prisma Schema

model Asset {
  id        String   @id @default(cuid())
  type      String   // Will store the asset type
  attributes      String   // JSON string storing the asset attributes
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  ownerId   String
  owner     User     @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)

  // Non-unique foreign key
  @@index([ownerId])
}

Create Asset Interfaces

interface BaseAsset {
  id: string;
  type: AssetTypeEnum;
  attributes: string; // JSON string
  createdAt: Date | string;
  updatedAt: Date | string;
  ownerId: string;
}

interface ImageAsset {
  altText?: string;
  contentType: string;
  blob: string;
}

interface PatternAsset {
  name: string;
  description?: string;
  blob: string;
}

// Define other interfaces similarly...

type AssetAttributes = ImageAsset | PatternAsset | GradientAsset; // Union of all asset interfaces

CRUD Operations

import { prisma } from '#app/utils/db.server';
import { z } from 'zod';

// Validation function
const validateAssetAttributes = (type: AssetTypeEnum, attributes: unknown) => {
  switch (type) {
    case AssetTypeEnum.IMAGE:
      return ImageAssetSchema.parse(attributes);
    case AssetTypeEnum.PATTERN:
      return PatternAssetSchema.parse(attributes);
    case AssetTypeEnum.GRADIENT:
      return GradientAssetSchema.parse(attributes);
    // Add other cases here...
    default:
      throw new Error('Unsupported asset type');
  }
};

// Create new asset
export const createAsset = async (ownerId: string, type: AssetTypeEnum, attributes: unknown) => {
  const validatedAttributes = validateAssetData(type, attributes);
  const jsonString = JSON.stringify(validatedAttributes);

  return await prisma.asset.create({
    data: {
      type,
      attributes: jsonString,
      ownerId,
    },
  });
};

Validation Functions

Create functions to validate JSON data using Zod before storing it in the database. This ensures that the attributes data conforms to the expected structure.

goodeats commented 4 months ago

example of what the json string data for designs could look like:

// Palette
{
  "format": "hex",
  "value": "000000",
  "opacity": 1.0
}

// Size
{
  "format": "percent",
  "value": 10,
  "basis": "width"
}

// Fill
{
  "style": "solid",
  "value": "000000",
  "basis": "defined"
}

// Stroke
{
  "style": "solid",
  "value": "000000",
  "basis": "defined"
}

// Line
{
  "width": 1,
  "format": "pixel",
  "basis": "size"
}

// Rotate
{
  "value": 0,
  "basis": "defined"
}

// Layout
{
  "style": "random",
  "count": 1000,
  "rows": 9,
  "columns": 9
}

// Template
{
  "style": "triangle"
}
goodeats commented 4 months ago

https://github.com/goodeats/epic-pppaaattt.xyz/pull/175

it worked