community feature

This commit is contained in:
wass08
2026-02-11 15:23:18 +09:00
parent 4f9c4655e6
commit 2e8b663001
71 changed files with 1917 additions and 126 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
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@pascal-app/db",
"version": "0.0.0",
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./client": {
"types": "./src/client.ts",
"default": "./src/client.ts"
},
"./server": {
"types": "./src/server.ts",
"default": "./src/server.ts"
},
"./types": {
"types": "./src/types.ts",
"default": "./src/types.ts"
}
},
"dependencies": {
"@supabase/supabase-js": "^2.95.3",
"drizzle-orm": "^0.39.0",
"drizzle-zod": "^0.5.1",
"nanoid": "^5.0.9",
"postgres": "^3.4.5"
},
"devDependencies": {
"@repo/typescript-config": "*",
"drizzle-kit": "^0.30.0",
"typescript": "5.9.2"
}
}
+17
View File
@@ -0,0 +1,17 @@
import { createClient } from '@supabase/supabase-js'
import type { Database } from './types'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
'Missing Supabase environment variables. Add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY to your .env.local file.'
)
}
/**
* Supabase client for client-side use with anon key
* Uses Row Level Security (RLS) policies
*/
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)
+17
View File
@@ -0,0 +1,17 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
if (!process.env.POSTGRES_URL) {
throw new Error(
'Missing POSTGRES_URL environment variable. Add your Supabase database connection string.',
)
}
// Create postgres connection
const client = postgres(process.env.POSTGRES_URL)
// Create drizzle instance
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 Supabase clients, Drizzle ORM, and types
*/
export { supabase } from './client'
export { supabaseAdmin } from './server'
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'
+31
View File
@@ -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)
+15
View File
@@ -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)
+28
View File
@@ -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 property for the session context
activePropertyId: t.text('active_property_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)
+28
View File
@@ -0,0 +1,28 @@
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'),
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))],
).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)
+11
View File
@@ -0,0 +1,11 @@
// Auth tables
export * from './auth/accounts'
export * from './auth/jwks'
export * from './auth/sessions'
export * from './auth/users'
export * from './auth/verifications'
// Property tables
export * from './properties/addresses'
export * from './properties/models'
export * from './properties/properties'
@@ -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(
'properties_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,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 { properties } from './properties'
export const models = pgTable('properties_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),
propertyId: t
.text('property_id')
.references(() => properties.id, { onDelete: 'set null' }),
sceneGraph: t.jsonb('scene_graph'),
metadata: t.jsonb('metadata'),
...timestampsColumnsSoftDelete,
})).enableRLS()
export const modelsRelations = relations(models, ({ one }) => ({
property: one(properties, {
fields: [models.propertyId],
references: [properties.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,44 @@
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 properties = pgTable(
'properties',
(t) => ({
id: id('property'),
name: t.text('name'),
addressId: t
.text('address_id')
.references(() => addresses.id, { onDelete: 'set null' })
.unique(),
ownerId: t
.text('owner_id')
.references(() => users.id, { onDelete: 'set null' }),
detailsJson: t.jsonb('details_json'),
metadata: t.jsonb('metadata'),
...timestampsColumns,
}),
(t) => [
index('property_address_idx').on(t.addressId),
index('property_owner_idx').on(t.ownerId),
],
).enableRLS()
export const propertiesRelations = relations(properties, ({ one }) => ({
address: one(addresses, {
fields: [properties.addressId],
references: [addresses.id],
}),
owner: one(users, {
fields: [properties.ownerId],
references: [users.id],
}),
}))
export type Property = typeof properties.$inferSelect
export type NewProperty = typeof properties.$inferInsert
export const insertPropertySchema = createInsertSchema(properties)
export const selectPropertySchema = createSelectSchema(properties)
+23
View File
@@ -0,0 +1,23 @@
import { createClient } from '@supabase/supabase-js'
import type { Database } from './types'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!supabaseUrl || !supabaseServiceRoleKey) {
throw new Error(
'Missing Supabase environment variables. Add NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY to your .env.local file.'
)
}
/**
* Supabase client for server-side use with service role key
* Bypasses Row Level Security (RLS) - use with caution
* Always filter by user_id to enforce permissions
*/
export const supabaseAdmin = createClient<Database>(supabaseUrl, supabaseServiceRoleKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
},
})
+124
View File
@@ -0,0 +1,124 @@
/**
* 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: {
properties: {
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
}
}
properties_addresses: {
Row: {
id: string
property_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
property_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
property_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
}
}
properties_models: {
Row: {
id: string
property_id: string
name: string
version: number
draft: boolean
scene_graph: Json | null
created_at: string
updated_at: string
}
Insert: {
id?: string
property_id: string
name: string
version?: number
draft?: boolean
scene_graph?: Json | null
created_at?: string
updated_at?: string
}
Update: {
id?: string
property_id?: string
name?: string
version?: number
draft?: boolean
scene_graph?: Json | null
created_at?: string
updated_at?: string
}
}
}
Views: {}
Functions: {}
Enums: {}
}
}
@@ -0,0 +1 @@
main
+1
View File
@@ -0,0 +1 @@
v2.75.0
+38
View File
@@ -0,0 +1,38 @@
# Supabase local development configuration
# This file is used by `supabase start` for local development
project_id = "pascal-editor"
[api]
enabled = true
port = 54321
schemas = ["public"]
extra_search_path = ["public"]
max_rows = 1000
[db]
port = 54322
shadow_port = 54320
major_version = 17
[studio]
enabled = true
port = 54323
[inbucket]
enabled = true
port = 54324
smtp_port = 54325
pop3_port = 54326
[auth]
enabled = true
site_url = "http://localhost:3000"
additional_redirect_urls = ["http://localhost:3000"]
jwt_expiry = 3600
enable_signup = true
[auth.email]
enable_signup = true
double_confirm_changes = false
enable_confirmations = false
@@ -0,0 +1,80 @@
-- Better Auth tables (copied from working monorepo)
-- Using auth_ prefix and snake_case columns
CREATE TYPE "public"."auth_user_roles" AS ENUM('user', 'admin');
CREATE TABLE "auth_accounts" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"provider_id" text NOT NULL,
"account_id" text NOT NULL,
"password" text,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp with time zone,
"refresh_token_expires_at" timestamp with time zone,
"scope" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "auth_accounts" ENABLE ROW LEVEL SECURITY;
CREATE TABLE "auth_jwks" (
"id" text PRIMARY KEY NOT NULL,
"public_key" text NOT NULL,
"private_key" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "auth_jwks" ENABLE ROW LEVEL SECURITY;
CREATE TABLE "auth_sessions" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"expires_at" timestamp with time zone,
"token" text NOT NULL,
"ip_address" text,
"user_agent" text,
"impersonated_by" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "auth_sessions" ENABLE ROW LEVEL SECURITY;
CREATE TABLE "auth_users" (
"id" text PRIMARY KEY NOT NULL,
"email" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"name" text NOT NULL,
"image" text,
"role" "auth_user_roles" DEFAULT 'user' NOT NULL,
"banned" boolean DEFAULT false NOT NULL,
"ban_reason" text,
"ban_expires" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "auth_users" ENABLE ROW LEVEL SECURITY;
CREATE TABLE "auth_verifications" (
"id" text PRIMARY KEY NOT NULL,
"value" text NOT NULL,
"identifier" text NOT NULL,
"expires_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "auth_verifications" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "auth_accounts" ADD CONSTRAINT "auth_accounts_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_impersonated_by_auth_users_id_fk" FOREIGN KEY ("impersonated_by") REFERENCES "public"."auth_users"("id") ON DELETE set null ON UPDATE no action;
CREATE UNIQUE INDEX "email_unique_index" ON "auth_users" USING btree (lower("email"));
@@ -0,0 +1,165 @@
-- Properties tables
-- Simple property management without organizations
-- Properties addresses table (must be created first due to foreign key)
CREATE TABLE IF NOT EXISTS properties_addresses (
id TEXT PRIMARY KEY,
street_number TEXT,
route TEXT,
route_short TEXT,
neighborhood TEXT,
city TEXT,
county TEXT,
state TEXT,
state_long TEXT,
postal_code TEXT,
postal_code_suffix TEXT,
country TEXT,
country_long TEXT,
latitude NUMERIC,
longitude NUMERIC,
raw_json JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(street_number, route, city, state, postal_code)
);
CREATE INDEX IF NOT EXISTS idx_properties_addresses_city_state ON properties_addresses(city, state);
-- Properties table
CREATE TABLE IF NOT EXISTS properties (
id TEXT PRIMARY KEY,
name TEXT,
address_id TEXT UNIQUE REFERENCES properties_addresses(id) ON DELETE SET NULL,
owner_id TEXT REFERENCES auth_users(id) ON DELETE SET NULL,
details_json JSONB,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_properties_owner_id ON properties(owner_id);
CREATE INDEX IF NOT EXISTS idx_properties_address_id ON properties(address_id);
-- Properties models table (scene graphs)
CREATE TABLE IF NOT EXISTS properties_models (
id TEXT PRIMARY KEY,
name TEXT,
version INTEGER DEFAULT 1,
description TEXT,
draft BOOLEAN DEFAULT true,
property_id TEXT REFERENCES properties(id) ON DELETE SET NULL,
scene_graph JSONB,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_properties_models_property_id ON properties_models(property_id);
CREATE INDEX IF NOT EXISTS idx_properties_models_version ON properties_models(property_id, version DESC);
-- Enable Row Level Security (RLS)
ALTER TABLE properties ENABLE ROW LEVEL SECURITY;
ALTER TABLE properties_addresses ENABLE ROW LEVEL SECURITY;
ALTER TABLE properties_models ENABLE ROW LEVEL SECURITY;
-- RLS Policies for properties table
CREATE POLICY "Users can view their own properties"
ON properties FOR SELECT
USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
CREATE POLICY "Users can insert their own properties"
ON properties FOR INSERT
WITH CHECK (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
CREATE POLICY "Users can update their own properties"
ON properties FOR UPDATE
USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
CREATE POLICY "Users can delete their own properties"
ON properties FOR DELETE
USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
-- RLS Policies for properties_addresses table
-- Addresses table doesn't have property_id, so we'll allow all authenticated users
CREATE POLICY "Authenticated users can view all addresses"
ON properties_addresses FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can insert addresses"
ON properties_addresses FOR INSERT
WITH CHECK (true);
CREATE POLICY "Authenticated users can update addresses"
ON properties_addresses FOR UPDATE
USING (true);
CREATE POLICY "Authenticated users can delete addresses"
ON properties_addresses FOR DELETE
USING (true);
-- RLS Policies for properties_models table
CREATE POLICY "Users can view models of their own properties"
ON properties_models FOR SELECT
USING (
EXISTS (
SELECT 1 FROM properties
WHERE properties.id = properties_models.property_id
AND properties.owner_id = current_setting('app.user_id', true)::TEXT
)
);
CREATE POLICY "Users can insert models for their own properties"
ON properties_models FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM properties
WHERE properties.id = properties_models.property_id
AND properties.owner_id = current_setting('app.user_id', true)::TEXT
)
);
CREATE POLICY "Users can update models of their own properties"
ON properties_models FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM properties
WHERE properties.id = properties_models.property_id
AND properties.owner_id = current_setting('app.user_id', true)::TEXT
)
);
CREATE POLICY "Users can delete models of their own properties"
ON properties_models FOR DELETE
USING (
EXISTS (
SELECT 1 FROM properties
WHERE properties.id = properties_models.property_id
AND properties.owner_id = current_setting('app.user_id', true)::TEXT
)
);
-- Trigger to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_properties_updated_at
BEFORE UPDATE ON properties
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_properties_addresses_updated_at
BEFORE UPDATE ON properties_addresses
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_properties_models_updated_at
BEFORE UPDATE ON properties_models
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
@@ -0,0 +1,2 @@
-- Add active_property_id to sessions table
ALTER TABLE auth_sessions ADD COLUMN IF NOT EXISTS active_property_id TEXT;
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}