rename before moving to monorepo
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# @pascal-app/db
|
||||
|
||||
Database package for Pascal Editor with Supabase.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
From the monorepo root:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
This installs Supabase CLI as a dev dependency.
|
||||
|
||||
### 2. Start Supabase locally
|
||||
|
||||
From the monorepo root:
|
||||
|
||||
```bash
|
||||
bun db:start
|
||||
```
|
||||
|
||||
This will start a local Supabase instance with PostgreSQL, PostgREST, and Studio.
|
||||
|
||||
### 3. Check Supabase status
|
||||
|
||||
```bash
|
||||
bun db:status
|
||||
```
|
||||
|
||||
You'll see output like:
|
||||
|
||||
```
|
||||
API URL: http://127.0.0.1:54321
|
||||
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
||||
Studio URL: http://127.0.0.1:54323
|
||||
Anon key: eyJh...
|
||||
Service role key: eyJh...
|
||||
```
|
||||
|
||||
### 4. Configure environment variables
|
||||
|
||||
Add these to `apps/editor/.env.local`:
|
||||
|
||||
```bash
|
||||
# Supabase
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_anon_key>
|
||||
SUPABASE_SERVICE_ROLE_KEY=<your_service_role_key>
|
||||
|
||||
# Better Auth
|
||||
BETTER_AUTH_SECRET=<generate_with_openssl_rand_base64_32>
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
Generate a secret for `BETTER_AUTH_SECRET`:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrations are located in `supabase/migrations/`.
|
||||
|
||||
### Apply migrations
|
||||
|
||||
```bash
|
||||
bun db:reset # Resets and applies all migrations
|
||||
```
|
||||
|
||||
### Create a new migration
|
||||
|
||||
```bash
|
||||
cd packages/db
|
||||
bunx supabase migration new <migration_name>
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Auth Tables (Better Auth)
|
||||
|
||||
- `users` - User accounts
|
||||
- `sessions` - Active sessions
|
||||
- `accounts` - OAuth provider accounts
|
||||
- `verification_tokens` - Magic link tokens
|
||||
|
||||
### Application Tables
|
||||
|
||||
- `properties` - User properties
|
||||
- `properties_addresses` - Property addresses with Google Maps data
|
||||
- `properties_models` - Scene graph models (versions)
|
||||
|
||||
## Usage
|
||||
|
||||
### Client-side (with RLS)
|
||||
|
||||
```typescript
|
||||
import { supabase } from '@pascal-app/db/client'
|
||||
|
||||
// RLS policies automatically filter by authenticated user
|
||||
const { data } = await supabase.from('properties').select('*')
|
||||
```
|
||||
|
||||
### Server-side (service role)
|
||||
|
||||
```typescript
|
||||
import { supabaseAdmin } from '@pascal-app/db/server'
|
||||
|
||||
// Bypasses RLS - you must manually filter by user_id
|
||||
const { data } = await supabaseAdmin
|
||||
.from('properties')
|
||||
.select('*')
|
||||
.eq('owner_id', userId)
|
||||
```
|
||||
|
||||
## Supabase Studio
|
||||
|
||||
Access the local Supabase Studio at: http://127.0.0.1:54323
|
||||
|
||||
Use this to:
|
||||
- Browse tables and data
|
||||
- Run SQL queries
|
||||
- View logs
|
||||
- Manage RLS policies
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
|
||||
// Keep this in sync with `supabase/config.toml` -> `[db].port`.
|
||||
const LOCAL_SUPABASE_DB_URL = 'postgresql://postgres:postgres@127.0.0.1:55322/postgres'
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/schema/index.ts',
|
||||
out: '../../supabase/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.POSTGRES_URL ?? LOCAL_SUPABASE_DB_URL,
|
||||
},
|
||||
schemaFilter: ['public'],
|
||||
introspect: {
|
||||
casing: 'camel',
|
||||
},
|
||||
migrations: {
|
||||
prefix: 'timestamp',
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@pascal-app/db",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"db:generate": "drizzle-kit generate --config drizzle.config.ts",
|
||||
"db:migrate": "bunx supabase --workdir ../.. db push --local",
|
||||
"db:push": "drizzle-kit push --config drizzle.config.ts",
|
||||
"db:studio": "drizzle-kit studio --config drizzle.config.ts"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.95.3",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"nanoid": "^5.0.9",
|
||||
"postgres": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/typescript-config": "*",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
import * as schema from './schema'
|
||||
|
||||
const connectionString = process.env.POSTGRES_URL ?? ''
|
||||
|
||||
const client = postgres(connectionString, { prepare: false })
|
||||
|
||||
export const db = drizzle({ client, schema })
|
||||
|
||||
export type Database = typeof db
|
||||
@@ -0,0 +1,56 @@
|
||||
import { type AnyColumn, type SQL, sql } from 'drizzle-orm'
|
||||
import { text, timestamp } from 'drizzle-orm/pg-core'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
|
||||
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
const nanoid = customAlphabet(alphabet, 16)
|
||||
|
||||
/**
|
||||
* Generate a unique ID with optional prefix
|
||||
* @example createId('user') => 'user_Abc123...'
|
||||
*/
|
||||
export const createId = (prefix?: string) => {
|
||||
const id = nanoid()
|
||||
return prefix ? `${prefix}_${id}` : id
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary key column with auto-generated prefixed ID
|
||||
* @example id('user') => text('id').notNull().primaryKey().$defaultFn(() => createId('user'))
|
||||
*/
|
||||
export const id = (prefix?: string) =>
|
||||
text('id')
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId(prefix))
|
||||
.$type<string>()
|
||||
|
||||
export const createdAt = timestamp('created_at', { withTimezone: true }).notNull().defaultNow()
|
||||
|
||||
export const updatedAt = timestamp('updated_at', { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
|
||||
export const deletedAt = timestamp('deleted_at', { withTimezone: true })
|
||||
|
||||
/**
|
||||
* Standard timestamp columns for created_at and updated_at
|
||||
*/
|
||||
export const timestamps = {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
}
|
||||
|
||||
// Alias for backwards compatibility with existing code
|
||||
export const timestampsColumns = timestamps
|
||||
|
||||
export const timestampsColumnsSoftDelete = {
|
||||
...timestampsColumns,
|
||||
deletedAt,
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL helper for case-insensitive comparison
|
||||
*/
|
||||
export const lower = (column: AnyColumn): SQL => sql`lower(${column})`
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Database package
|
||||
* Exports Drizzle ORM and types
|
||||
* Note: Supabase clients are kept in the app's lib directory to avoid build-time initialization
|
||||
*/
|
||||
|
||||
export type { Database as SupabaseDatabase } from './types'
|
||||
|
||||
// Drizzle exports
|
||||
export { type Database, db } from './drizzle'
|
||||
export * from './schema'
|
||||
|
||||
|
||||
import * as dbSchema from './schema'
|
||||
export const schema = dbSchema
|
||||
export {
|
||||
createId,
|
||||
deletedAt,
|
||||
id,
|
||||
lower,
|
||||
timestamps,
|
||||
timestampsColumns,
|
||||
timestampsColumnsSoftDelete,
|
||||
} from './helpers'
|
||||
@@ -0,0 +1,31 @@
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestamps } from '../../helpers'
|
||||
import { users } from './users'
|
||||
|
||||
export const accounts = pgTable('auth_accounts', (t) => ({
|
||||
id: id('account'),
|
||||
userId: t
|
||||
.text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
providerId: t.text('provider_id').notNull(),
|
||||
accountId: t.text('account_id').notNull(),
|
||||
password: t.text('password'),
|
||||
accessToken: t.text('access_token'),
|
||||
refreshToken: t.text('refresh_token'),
|
||||
idToken: t.text('id_token'),
|
||||
accessTokenExpiresAt: t.timestamp('access_token_expires_at', {
|
||||
withTimezone: true,
|
||||
}),
|
||||
refreshTokenExpiresAt: t.timestamp('refresh_token_expires_at', {
|
||||
withTimezone: true,
|
||||
}),
|
||||
scope: t.text('scope'),
|
||||
...timestamps,
|
||||
})).enableRLS()
|
||||
|
||||
export type Account = typeof accounts.$inferSelect
|
||||
export type NewAccount = typeof accounts.$inferInsert
|
||||
export const insertAccountSchema = createInsertSchema(accounts)
|
||||
export const selectAccountSchema = createSelectSchema(accounts)
|
||||
@@ -0,0 +1,15 @@
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { createdAt, id } from '../../helpers'
|
||||
|
||||
export const jwks = pgTable('auth_jwks', (t) => ({
|
||||
id: id('jwks'),
|
||||
publicKey: t.text('public_key').notNull(),
|
||||
privateKey: t.text('private_key').notNull(),
|
||||
createdAt,
|
||||
})).enableRLS()
|
||||
|
||||
export type Jwks = typeof jwks.$inferSelect
|
||||
export type NewJwks = typeof jwks.$inferInsert
|
||||
export const insertJwksSchema = createInsertSchema(jwks)
|
||||
export const selectJwksSchema = createSelectSchema(jwks)
|
||||
@@ -0,0 +1,28 @@
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestamps } from '../../helpers'
|
||||
import { users } from './users'
|
||||
|
||||
export const sessions = pgTable('auth_sessions', (t) => ({
|
||||
id: id('session'),
|
||||
userId: t
|
||||
.text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
expiresAt: t.timestamp('expires_at', { withTimezone: true }),
|
||||
token: t.text('token').notNull(),
|
||||
ipAddress: t.text('ip_address'),
|
||||
userAgent: t.text('user_agent'),
|
||||
// Custom: active project for the session context
|
||||
activeProjectId: t.text('active_project_id'),
|
||||
// Admin plugin support: tracks who is impersonating this session
|
||||
impersonatedBy: t
|
||||
.text('impersonated_by')
|
||||
.references(() => users.id, { onDelete: 'set null' }),
|
||||
...timestamps,
|
||||
})).enableRLS()
|
||||
|
||||
export type Session = typeof sessions.$inferSelect
|
||||
export type NewSession = typeof sessions.$inferInsert
|
||||
export const insertSessionSchema = createInsertSchema(sessions)
|
||||
export const selectSessionSchema = createSelectSchema(sessions)
|
||||
@@ -0,0 +1,41 @@
|
||||
import { pgEnum, pgTable, uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, lower, timestampsColumns } from '../../helpers'
|
||||
|
||||
export const USER_ROLES = ['user', 'admin'] as const
|
||||
export const userRoles = pgEnum('auth_user_roles', USER_ROLES)
|
||||
|
||||
export const users = pgTable(
|
||||
'auth_users',
|
||||
(t) => ({
|
||||
id: id('user'),
|
||||
email: t.text('email').notNull(),
|
||||
emailVerified: t.boolean('email_verified').notNull().default(false),
|
||||
name: t.text('name').notNull(),
|
||||
image: t.text('image'),
|
||||
/** Public username for the community hub */
|
||||
username: t.text('username'),
|
||||
/** GitHub profile URL */
|
||||
githubUrl: t.text('github_url'),
|
||||
/** X/Twitter profile URL */
|
||||
xUrl: t.text('x_url'),
|
||||
/** YouTube channel URL */
|
||||
youtubeUrl: t.text('youtube_url'),
|
||||
/** Whether the user wants to receive email notifications about new features and updates */
|
||||
emailNotifications: t.boolean('email_notifications').notNull().default(true),
|
||||
role: userRoles('role').notNull().default('user'),
|
||||
banned: t.boolean('banned').notNull().default(false),
|
||||
banReason: t.text('ban_reason'),
|
||||
banExpires: t.timestamp('ban_expires', { withTimezone: true }),
|
||||
...timestampsColumns,
|
||||
}),
|
||||
(t) => [
|
||||
uniqueIndex('email_unique_index').on(lower(t.email)),
|
||||
uniqueIndex('username_unique_index').on(lower(t.username)),
|
||||
],
|
||||
).enableRLS()
|
||||
|
||||
export type User = typeof users.$inferSelect
|
||||
export type NewUser = typeof users.$inferInsert
|
||||
export const insertUserSchema = createInsertSchema(users)
|
||||
export const selectUserSchema = createSelectSchema(users)
|
||||
@@ -0,0 +1,22 @@
|
||||
import { index, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestamps } from '../../helpers'
|
||||
|
||||
export const verifications = pgTable(
|
||||
'auth_verifications',
|
||||
(t) => ({
|
||||
id: id('verification'),
|
||||
value: t.text('value').notNull(),
|
||||
identifier: t.text('identifier').notNull(),
|
||||
expiresAt: t.timestamp('expires_at', {
|
||||
withTimezone: true,
|
||||
}),
|
||||
...timestamps,
|
||||
}),
|
||||
(t) => [index('verification_identifier_index').on(t.identifier)],
|
||||
).enableRLS()
|
||||
|
||||
export type Verification = typeof verifications.$inferSelect
|
||||
export type NewVerification = typeof verifications.$inferInsert
|
||||
export const insertVerificationSchema = createInsertSchema(verifications)
|
||||
export const selectVerificationSchema = createSelectSchema(verifications)
|
||||
@@ -0,0 +1,18 @@
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, createdAt } from '../../helpers'
|
||||
|
||||
export const feedback = pgTable('feedback', (t) => ({
|
||||
id: id('feedback'),
|
||||
userId: t.text('user_id'),
|
||||
projectId: t.text('project_id'),
|
||||
message: t.text('message').notNull(),
|
||||
images: t.jsonb('images').$type<string[]>(),
|
||||
sceneGraph: t.jsonb('scene_graph'),
|
||||
createdAt,
|
||||
})).enableRLS()
|
||||
|
||||
export type Feedback = typeof feedback.$inferSelect
|
||||
export type NewFeedback = typeof feedback.$inferInsert
|
||||
export const insertFeedbackSchema = createInsertSchema(feedback)
|
||||
export const selectFeedbackSchema = createSelectSchema(feedback)
|
||||
@@ -0,0 +1,19 @@
|
||||
// Auth tables
|
||||
export * from './auth/accounts'
|
||||
export * from './auth/jwks'
|
||||
export * from './auth/sessions'
|
||||
export * from './auth/users'
|
||||
export * from './auth/verifications'
|
||||
|
||||
// Feedback table
|
||||
export * from './feedback/feedback'
|
||||
|
||||
// Project tables
|
||||
export * from './projects/addresses'
|
||||
export * from './projects/assets'
|
||||
export * from './projects/likes'
|
||||
export * from './projects/models'
|
||||
export * from './projects/projects'
|
||||
|
||||
// Presets table
|
||||
export * from './presets/presets'
|
||||
@@ -0,0 +1,30 @@
|
||||
import { pgTable, index, text, boolean, jsonb } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestampsColumns } from '../../helpers'
|
||||
import { users } from '../auth/users'
|
||||
|
||||
export const presets = pgTable(
|
||||
'presets',
|
||||
(t) => ({
|
||||
id: id('preset'),
|
||||
type: t.text('type').notNull(), // 'door' | 'window'
|
||||
name: t.text('name').notNull(),
|
||||
data: t.jsonb('data').notNull(),
|
||||
thumbnailUrl: t.text('thumbnail_url'),
|
||||
userId: t
|
||||
.text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
isCommunity: t.boolean('is_community').notNull().default(false),
|
||||
...timestampsColumns,
|
||||
}),
|
||||
(t) => [
|
||||
index('presets_type_idx').on(t.type),
|
||||
index('presets_user_id_idx').on(t.userId),
|
||||
index('presets_is_community_idx').on(t.isCommunity),
|
||||
],
|
||||
).enableRLS()
|
||||
|
||||
export type Preset = typeof presets.$inferSelect
|
||||
export type NewPreset = typeof presets.$inferInsert
|
||||
export const insertPresetSchema = createInsertSchema(presets)
|
||||
export const selectPresetSchema = createSelectSchema(presets)
|
||||
@@ -0,0 +1,53 @@
|
||||
import { pgTable, unique } from 'drizzle-orm/pg-core'
|
||||
import { z } from 'zod'
|
||||
import { id, timestampsColumns } from '../../helpers'
|
||||
|
||||
export const addresses = pgTable(
|
||||
'projects_addresses',
|
||||
(t) => ({
|
||||
id: id('address'),
|
||||
streetNumber: t.text('street_number'),
|
||||
route: t.text('route'),
|
||||
routeShort: t.text('route_short'),
|
||||
neighborhood: t.text('neighborhood'),
|
||||
city: t.text('city'),
|
||||
county: t.text('county'),
|
||||
state: t.text('state'),
|
||||
stateLong: t.text('state_long'),
|
||||
postalCode: t.text('postal_code'),
|
||||
postalCodeSuffix: t.text('postal_code_suffix'),
|
||||
country: t.text('country'),
|
||||
countryLong: t.text('country_long'),
|
||||
latitude: t.numeric('latitude'),
|
||||
longitude: t.numeric('longitude'),
|
||||
rawJson: t.jsonb('raw_json'),
|
||||
...timestampsColumns,
|
||||
}),
|
||||
(t) => [
|
||||
// Unique constraint on core address components to prevent duplicates
|
||||
unique('address_components_unique').on(t.streetNumber, t.route, t.city, t.state, t.postalCode),
|
||||
],
|
||||
).enableRLS()
|
||||
|
||||
// Create address schema manually to avoid issues with generated columns
|
||||
export const addressSchema = z.object({
|
||||
streetNumber: z.string().optional(),
|
||||
route: z.string().optional(),
|
||||
routeShort: z.string().optional(),
|
||||
neighborhood: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
county: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
stateLong: z.string().optional(),
|
||||
postalCode: z.string().optional(),
|
||||
postalCodeSuffix: z.string().optional(),
|
||||
country: z.string().default('US'),
|
||||
countryLong: z.string().optional(),
|
||||
latitude: z.string().optional(),
|
||||
longitude: z.string().optional(),
|
||||
rawJson: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export type AddressSchema = z.infer<typeof addressSchema>
|
||||
export type Address = typeof addresses.$inferSelect
|
||||
export type NewAddress = typeof addresses.$inferInsert
|
||||
@@ -0,0 +1,25 @@
|
||||
import { relations } from 'drizzle-orm'
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { id, timestampsColumns } from '../../helpers'
|
||||
import { projects } from './projects'
|
||||
|
||||
export const projectAssets = pgTable('project_assets', (t) => ({
|
||||
id: id('asset'),
|
||||
projectId: t.text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
||||
storageKey: t.text('storage_key').notNull(),
|
||||
url: t.text('url').notNull(),
|
||||
type: t.text('type').notNull(), // 'scan' | 'guide'
|
||||
originalName: t.text('original_name'),
|
||||
mimeType: t.text('mime_type'),
|
||||
...timestampsColumns,
|
||||
})).enableRLS()
|
||||
|
||||
export const projectAssetsRelations = relations(projectAssets, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [projectAssets.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export type ProjectAsset = typeof projectAssets.$inferSelect
|
||||
export type NewProjectAsset = typeof projectAssets.$inferInsert
|
||||
@@ -0,0 +1,36 @@
|
||||
import { relations } from 'drizzle-orm'
|
||||
import { pgTable, unique } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, createdAt } from '../../helpers'
|
||||
import { projects } from './projects'
|
||||
import { users } from '../auth/users'
|
||||
|
||||
export const projectsLikes = pgTable(
|
||||
'projects_likes',
|
||||
(t) => ({
|
||||
id: id('like'),
|
||||
projectId: t
|
||||
.text('project_id')
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: 'cascade' }),
|
||||
userId: t
|
||||
.text('user_id')
|
||||
.notNull(),
|
||||
createdAt,
|
||||
}),
|
||||
(t) => [
|
||||
unique('projects_likes_project_user_unique').on(t.projectId, t.userId),
|
||||
],
|
||||
).enableRLS()
|
||||
|
||||
export const projectsLikesRelations = relations(projectsLikes, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [projectsLikes.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export type ProjectLike = typeof projectsLikes.$inferSelect
|
||||
export type NewProjectLike = typeof projectsLikes.$inferInsert
|
||||
export const insertProjectLikeSchema = createInsertSchema(projectsLikes)
|
||||
export const selectProjectLikeSchema = createSelectSchema(projectsLikes)
|
||||
@@ -0,0 +1,31 @@
|
||||
import { relations } from 'drizzle-orm'
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestampsColumnsSoftDelete } from '../../helpers'
|
||||
import { projects } from './projects'
|
||||
|
||||
export const models = pgTable('projects_models', (t) => ({
|
||||
id: id('model'),
|
||||
name: t.text('name'),
|
||||
version: t.integer('version').default(1),
|
||||
description: t.text('description'),
|
||||
draft: t.boolean('draft').default(true),
|
||||
projectId: t
|
||||
.text('project_id')
|
||||
.references(() => projects.id, { onDelete: 'set null' }),
|
||||
sceneGraph: t.jsonb('scene_graph'),
|
||||
metadata: t.jsonb('metadata'),
|
||||
...timestampsColumnsSoftDelete,
|
||||
})).enableRLS()
|
||||
|
||||
export const modelsRelations = relations(models, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [models.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export type Model = typeof models.$inferSelect
|
||||
export type NewModel = typeof models.$inferInsert
|
||||
export const insertModelSchema = createInsertSchema(models)
|
||||
export const selectModelSchema = createSelectSchema(models)
|
||||
@@ -0,0 +1,55 @@
|
||||
import { relations } from 'drizzle-orm'
|
||||
import { index, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestampsColumns } from '../../helpers'
|
||||
import { users } from '../auth/users'
|
||||
import { addresses } from './addresses'
|
||||
|
||||
export const projects = pgTable(
|
||||
'projects',
|
||||
(t) => ({
|
||||
id: id('project'),
|
||||
name: t.text('name'),
|
||||
addressId: t
|
||||
.text('address_id')
|
||||
.references(() => addresses.id, { onDelete: 'set null' }),
|
||||
ownerId: t
|
||||
.text('owner_id')
|
||||
.references(() => users.id, { onDelete: 'set null' }),
|
||||
detailsJson: t.jsonb('details_json'),
|
||||
metadata: t.jsonb('metadata'),
|
||||
publishedModelVersion: t.integer('published_model_version'),
|
||||
// Community features
|
||||
isPrivate: t.boolean('is_private').notNull().default(true),
|
||||
isEmpty: t.boolean('is_empty').notNull().default(true),
|
||||
showScansPublic: t.boolean('show_scans_public').notNull().default(true),
|
||||
showGuidesPublic: t.boolean('show_guides_public').notNull().default(true),
|
||||
views: t.integer('views').notNull().default(0),
|
||||
likes: t.integer('likes').notNull().default(0),
|
||||
thumbnailUrl: t.text('thumbnail_url'),
|
||||
...timestampsColumns,
|
||||
}),
|
||||
(t) => [
|
||||
index('project_address_idx').on(t.addressId),
|
||||
index('project_owner_idx').on(t.ownerId),
|
||||
index('project_is_private_idx').on(t.isPrivate),
|
||||
index('project_views_idx').on(t.views),
|
||||
index('project_likes_idx').on(t.likes),
|
||||
],
|
||||
).enableRLS()
|
||||
|
||||
export const projectsRelations = relations(projects, ({ one }) => ({
|
||||
address: one(addresses, {
|
||||
fields: [projects.addressId],
|
||||
references: [addresses.id],
|
||||
}),
|
||||
owner: one(users, {
|
||||
fields: [projects.ownerId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export type Project = typeof projects.$inferSelect
|
||||
export type NewProject = typeof projects.$inferInsert
|
||||
export const insertProjectSchema = createInsertSchema(projects)
|
||||
export const selectProjectSchema = createSelectSchema(projects)
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Database types for Supabase
|
||||
* Generated from database schema
|
||||
*/
|
||||
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
||||
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
projects: {
|
||||
Row: {
|
||||
id: string
|
||||
name: string
|
||||
owner_id: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
name: string
|
||||
owner_id: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
name?: string
|
||||
owner_id?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
projects_addresses: {
|
||||
Row: {
|
||||
id: string
|
||||
project_id: string
|
||||
formatted_address: string
|
||||
street_number: string | null
|
||||
route: string | null
|
||||
locality: string | null
|
||||
administrative_area_level_1: string | null
|
||||
administrative_area_level_2: string | null
|
||||
country: string | null
|
||||
postal_code: string | null
|
||||
latitude: number | null
|
||||
longitude: number | null
|
||||
place_id: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
project_id: string
|
||||
formatted_address: string
|
||||
street_number?: string | null
|
||||
route?: string | null
|
||||
locality?: string | null
|
||||
administrative_area_level_1?: string | null
|
||||
administrative_area_level_2?: string | null
|
||||
country?: string | null
|
||||
postal_code?: string | null
|
||||
latitude?: number | null
|
||||
longitude?: number | null
|
||||
place_id?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
project_id?: string
|
||||
formatted_address?: string
|
||||
street_number?: string | null
|
||||
route?: string | null
|
||||
locality?: string | null
|
||||
administrative_area_level_1?: string | null
|
||||
administrative_area_level_2?: string | null
|
||||
country?: string | null
|
||||
postal_code?: string | null
|
||||
latitude?: number | null
|
||||
longitude?: number | null
|
||||
place_id?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
projects_models: {
|
||||
Row: {
|
||||
id: string
|
||||
project_id: string
|
||||
name: string
|
||||
version: number
|
||||
draft: boolean
|
||||
scene_graph: Json | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
project_id: string
|
||||
name: string
|
||||
version?: number
|
||||
draft?: boolean
|
||||
scene_graph?: Json | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
project_id?: string
|
||||
name?: string
|
||||
version?: number
|
||||
draft?: boolean
|
||||
scene_graph?: Json | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
presets: {
|
||||
Row: {
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
data: Json
|
||||
thumbnail_url: string | null
|
||||
user_id: string | null
|
||||
is_community: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
type: string
|
||||
name: string
|
||||
data: Json
|
||||
thumbnail_url?: string | null
|
||||
user_id?: string | null
|
||||
is_community?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
type?: string
|
||||
name?: string
|
||||
data?: Json
|
||||
thumbnail_url?: string | null
|
||||
user_id?: string | null
|
||||
is_community?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
Views: {}
|
||||
Functions: {}
|
||||
Enums: {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user