Skip to content

Commit

Permalink
FUCK my LIFE: This deviates from the simple idea I had in mine for
Browse files Browse the repository at this point in the history
uploads because I want to support image variants to resize original
images into the desired size. I'm following a bit Rails ActiveStorage
implementation but I saw they use polymorphic associations so I need to
see how I can manage it in Drizzle. This issue has some nice options:
drizzle-team/drizzle-orm#1051 (comment)

I think is duable.

Another consideration is in terms of adapters. Now is ultra harcoded to
PostgreSQL + Drizzle. But if I would like to expand to MySQL I should do
a kind of Adapter pattern like the one use Licia Auth or NextJS Auth. I
like the one use Licia because looks simple enough. Under the hood all
DB access is done with raw SQL which I think makes sense if I also want
to decouple from Drizzle DSL. Not sure if it's worth it.

In terms of API types I would like to keep something similar to what I
had in previous commit where was something like

const factory = AttachmentFactory({
  dbAdapter: PostgrsqlAdapter({
    client: db
  })
})

type Schema = typeof schema
UserAttachment = factory.build<Shchema['users']>({
  drive: disk, // DiskWrapper (filesystem or S3)
  table: schema['users'],
  // I would like something like this where relation is infered from
  // Drizzle schema relations
  // But this couple the thing with Drizzle which I'm kind of ok.
  // In this config also can go the variants config for the different
  // file sizes we want to generate.

  // In terms of variants Rails do on-deman variants when requesting
  // from the UI but it stores the variant after first request.
  // I'm not sure if I want to add Sharp (or something like it) to
  // support variants. Maybe this doesn't work in Vercel or Cloudflare
  // Edge. Pretty sure it doesn't

  // Other approach would to have a AWS Lambda that does the resize when
  // a file is addaded to the bucket. But this does the system DEV
  // friendly and also requires on the final user to implement the Lamda. I
  // can help here by implementing using IAC like SST ion

  // Both options have tradeoffs.
  attachments: [{ relation: 'avatar' }],
})

const attachment = UserAttachment.new({ id: 'some-user-id' })
// Optional if I want to get the user from DB
await user.getModel()
// Now that we have the user model we have methods for
add/delete/get/getUrl

// This creates relations in DB
// It also use Flydrive to store the file in Disk or S3
await attachment.addAvatar({ file: someFile })
  • Loading branch information
andresgutgon committed Jul 21, 2024
1 parent d056c50 commit c60475b
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 26 deletions.
4 changes: 2 additions & 2 deletions apps/web/src/db/attachments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const factory = AttachmentFactory<Blob, typeof schema>({
dbSchema: schema,
orm: db,
})
const users = factory.build<Schema['users']>({
const UserAttachment = factory.build<Schema['users']>({
table: schema['users'],
attachments: [{ relation: 'avatar' }],
})

export default { users }
export default { UserAttachment }
20 changes: 20 additions & 0 deletions apps/web/src/db/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import env from '$/env'
import { and, eq, getTableColumns } from 'drizzle-orm'
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'
import pg from 'pg'

Expand All @@ -14,4 +15,23 @@ export type Database = NodePgDatabase<typeof schema>
export type Schema = typeof schema
const db = drizzle(pool, { schema })

export async function getAvatarSql() {
const columns = getTableColumns(schema.blobs)
const result = db
.select(columns)
.from(schema.attachments)
.leftJoin(
schema.blobs,
eq(schema.attachments.id, schema.blobs.attachmentId),
)
.where(
and(
eq(schema.attachments.attachableType, schema.AttachableType.Users),
eq(schema.attachments.attachableId, 1n),
),
).toSQL()

return result.sql
}

export default db
25 changes: 25 additions & 0 deletions apps/web/src/db/schema/attachments/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { blobs } from '$/db/schema/attachments/blobs'
import readfort from '$/db/schema/dbSchema'
import { InferSelectModel, relations } from 'drizzle-orm'
import { bigserial } from 'drizzle-orm/pg-core'

export enum AttachableType {
Users = 'users',
}
export const attachableEnum = readfort.enum('attachable_type', [
AttachableType.Users,
])

export const attachments = readfort.table('attachments', {
id: bigserial('id', { mode: 'number' }).notNull(),
attachableId: bigserial('attachable_id', { mode: 'bigint' }).notNull(),
attachableType: attachableEnum('attachable_type').notNull(),
})

export const attachmentsRelations = relations(attachments, ({ many }) => ({
blobs: many(blobs),
}))

export type Attachment = InferSelectModel<typeof attachments> & {
blobs: Blob[]
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { attachments } from '$/db/schema/attachments/attachments'
import readfort from '$/db/schema/dbSchema'
import { timestamps } from '$/db/schema/schemaHelpers'
import { InferSelectModel, relations } from 'drizzle-orm'
Expand All @@ -7,16 +8,27 @@ export const blobs = readfort.table(
'blobs',
{
id: bigserial('id', { mode: 'bigint' }).notNull().primaryKey(),
blobName: varchar('blob_name', { length: 256 }),
key: varchar('key', { length: 256 }).notNull(),
contentType: varchar('content_type', { length: 256 }).notNull(),
contentLength: numeric('content_length').notNull(),
attachmentId: bigserial('attachment_id', { mode: 'bigint' }).references(
() => attachments.id,
{ onDelete: 'cascade' },
),
etag: varchar('etag', { length: 256 }).notNull(),
...timestamps(),
},
(blobs) => ({
blobKeyIdx: index('blob_key_idx').on(blobs.key),
}),
)
export const blobsRelations = relations(blobs, () => ({}))

export const blobsRelations = relations(blobs, ({ one }) => ({
attachment: one(attachments, {
fields: [blobs.attachmentId],
references: [attachments.id],
}),
}))

export type Blob = InferSelectModel<typeof blobs>
37 changes: 17 additions & 20 deletions apps/web/src/db/schema/auth/users.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import crypto from 'node:crypto'

import {
AttachableType,
Attachment,
attachments,
} from '$/db/schema/attachments/attachments'
import { Account, accounts } from '$/db/schema/auth/accounts'
import readfort from '$/db/schema/dbSchema'
import { Blob, blobs } from '$/db/schema/media/blobs'
import { lowercaseColumn, timestamps } from '$/db/schema/schemaHelpers'
import { KindleCountry } from '$/lib/types'
import { InferSelectModel, relations } from 'drizzle-orm'
import {
AnyPgColumn,
bigint,
text,
timestamp,
uniqueIndex,
varchar,
} from 'drizzle-orm/pg-core'
import { InferSelectModel, relations, sql } from 'drizzle-orm'
import { bigserial, text, timestamp, uniqueIndex, varchar } from 'drizzle-orm/pg-core'

export const kindleCountriesEnum = readfort.enum('kindle', [
KindleCountry.US,
Expand All @@ -32,18 +29,18 @@ export const kindleCountriesEnum = readfort.enum('kindle', [
export const users = readfort.table(
'users',
{
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
/* id: text('id') */
/* .primaryKey() */
/* .$defaultFn(() => crypto.randomUUID()), */
id: bigserial('id', { mode: 'number' }).notNull().primaryKey(),
name: text('name'),
email: text('email').notNull(),
username: varchar('username').notNull(),
emailVerified: timestamp('emailVerified', { mode: 'date' }),
image: text('image'),
kindle: kindleCountriesEnum('kindle'),
avatarId: bigint('avatar_id', { mode: 'bigint' }).references(
(): AnyPgColumn => blobs.id,
{ onDelete: 'set null' },
attachableType: varchar('attachable_type', { length: 256 }).default(
sql`'${AttachableType.Users}'`,
),
...timestamps(),
},
Expand All @@ -58,15 +55,15 @@ export const usersRelations = relations(users, ({ one }) => ({
fields: [users.id],
references: [accounts.userId],
}),
avatar: one(blobs, {
fields: [users.avatarId],
references: [blobs.id],
avatar: one(attachments, {
fields: [users.id, users.attachableType],
references: [attachments.attachableId, attachments.attachableType],
}),
}))

export type User = InferSelectModel<typeof users> & {
account?: Account
avatar?: Blob
avatar?: Attachment
}

export type SafeUser = Pick<
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/db/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export { default as readfortSchema } from './dbSchema'
export * from './auth/users'
export * from './auth/accounts'
export * from './auth/verificationTokens'
export * from './media/blobs'

// Media models
export * from './attachments/blobs'
export * from './attachments/attachments'
1 change: 1 addition & 0 deletions apps/web/src/lib/attachments/AttachmentFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { NodePgDatabase } from 'drizzle-orm/node-postgres'
import { PgTableWithColumns } from 'drizzle-orm/pg-core'
import { RelationalQueryBuilder } from 'drizzle-orm/pg-core/query-builders/query'

export function AttachmentFactory<
R extends object,
T extends GenericSchema = Record<string, unknown>,
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/lib/attachments/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
export { AttachmentFactory } from './AttachmentFactory'

/* // Then, declare types for our dynamic methods */
/* type DynamicAttachmentMethods<T> = { */
/* [P in `add${Capitalize<string & T>}` | `remove${Capitalize<string & T>}` | `get${Capitalize<string & T>}` | `get${Capitalize<string & T>}Url`]: () => void; */
/* }; */
/**/
/* // Factory function to create an Attachment instance with dynamic methods */
/* function createAttachment<T extends string>(rel: T): Attachment & DynamicAttachmentMethods<T> { */
/* const attachment = new Attachment(rel); */
/**/
/* const baseMethodName = `${rel.charAt(0).toUpperCase()}${rel.substring(1)}`; */
/* attachment[`add${baseMethodName}`] = function() { */
/* console.log(`Add called for ${rel}`); */
/* }; */
/* attachment[`remove${baseMethodName}`] = function() { */
/* console.log(`Remove called for ${rel}`); */
/* }; */
/* attachment[`get${baseMethodName}`] = function() { */
/* console.log(`Get called for ${rel}`); */
/* }; */
/* attachment[`get${baseMethodName}Url`] = function() { */
/* console.log(`GetUrl called for ${rel}`); */
/* }; */
/**/
/* return attachment as Attachment & DynamicAttachmentMethods<T>; */
/* } */
4 changes: 2 additions & 2 deletions apps/web/src/lib/attachments/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ExtractTablesWithRelations } from "drizzle-orm"
import { PgTableWithColumns } from "drizzle-orm/pg-core"
import { ExtractTablesWithRelations } from 'drizzle-orm'
import { PgTableWithColumns } from 'drizzle-orm/pg-core'

export type GenericSchema = Record<string, unknown>

Expand Down

0 comments on commit c60475b

Please sign in to comment.