rename before moving to monorepo

This commit is contained in:
wass08
2026-03-11 15:12:41 +01:00
parent 5bd9c21dbc
commit 3eeba322a8
29 changed files with 24 additions and 24 deletions
+127
View File
@@ -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
+22
View File
@@ -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,
})
+33
View File
@@ -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"
}
}
+11
View File
@@ -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
+56
View File
@@ -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})`
+24
View File
@@ -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)
+19
View File
@@ -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)
+160
View File
@@ -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: {}
}
}
+11
View File
@@ -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"]
}