From dbf81f5305e3a4ea0c4cc2da61beda92f26c17fe Mon Sep 17 00:00:00 2001 From: nichenqin Date: Tue, 14 Jan 2025 11:12:52 +0800 Subject: [PATCH] feat: support postgres --- .../drizzle/postgres/0000_low_jimmy_woo.sql | 277 +++ .../drizzle/postgres/meta/0000_snapshot.json | 2130 +++++++++++++++++ .../drizzle/postgres/meta/_journal.json | 13 + .../{ => sqlite}/0000_lively_warstar.sql | 0 .../0001_familiar_joshua_kane.sql | 0 .../{ => sqlite}/0002_fixed_lockjaw.sql | 0 .../{ => sqlite}/0003_dry_starhawk.sql | 0 .../{ => sqlite}/0004_tricky_phil_sheldon.sql | 0 .../drizzle/{ => sqlite}/0005_narrow_khan.sql | 0 .../{ => sqlite}/0006_mature_madame_web.sql | 0 .../{ => sqlite}/0007_steep_dragon_lord.sql | 0 .../{ => sqlite}/0008_bored_terror.sql | 0 .../{ => sqlite}/0009_workable_scorpion.sql | 0 .../{ => sqlite}/0010_nostalgic_nehzno.sql | 0 .../{ => sqlite}/0011_serious_marvex.sql | 0 .../{ => sqlite}/0012_lying_tomorrow_man.sql | 0 .../{ => sqlite}/0013_lovely_mordo.sql | 0 .../{ => sqlite}/0014_messy_sasquatch.sql | 0 .../{ => sqlite}/meta/0000_snapshot.json | 0 .../{ => sqlite}/meta/0001_snapshot.json | 0 .../{ => sqlite}/meta/0002_snapshot.json | 0 .../{ => sqlite}/meta/0003_snapshot.json | 0 .../{ => sqlite}/meta/0004_snapshot.json | 0 .../{ => sqlite}/meta/0005_snapshot.json | 0 .../{ => sqlite}/meta/0006_snapshot.json | 0 .../{ => sqlite}/meta/0007_snapshot.json | 0 .../{ => sqlite}/meta/0008_snapshot.json | 0 .../{ => sqlite}/meta/0009_snapshot.json | 0 .../{ => sqlite}/meta/0010_snapshot.json | 0 .../{ => sqlite}/meta/0011_snapshot.json | 0 .../{ => sqlite}/meta/0012_snapshot.json | 0 .../{ => sqlite}/meta/0013_snapshot.json | 0 .../{ => sqlite}/meta/0014_snapshot.json | 0 .../drizzle/{ => sqlite}/meta/_journal.json | 0 apps/backend/package.json | 4 + .../backend/src/modules/auth/auth.provider.ts | 27 +- apps/backend/src/modules/auth/auth.ts | 13 +- apps/backend/src/modules/auth/oauth/github.ts | 10 +- apps/backend/src/modules/auth/oauth/google.ts | 10 +- apps/backend/src/modules/auth/otp.service.ts | 3 +- .../backend/src/modules/space/space.module.ts | 6 +- apps/backend/src/registry/db.registry.ts | 21 +- apps/frontend/src/lib/registry.svelte.ts | 10 +- bun.lockb | Bin 600592 -> 609816 bytes drizzle.postgres.config.ts | 8 + drizzle.config.ts => drizzle.sqlite.config.ts | 4 +- drizzle.turso.config.ts | 4 +- package.json | 8 +- packages/env/src/index.ts | 12 +- packages/persistence/package.json | 3 + packages/persistence/src/client.ts | 2 + packages/persistence/src/ctx.ts | 4 +- packages/persistence/src/db-client.ts | 7 + packages/persistence/src/db.provider.ts | 5 + packages/persistence/src/db.ts | 2 +- packages/persistence/src/migrate.server.ts | 21 +- packages/persistence/src/qb.server.ts | 10 + .../record/record-query-creator-visitor.ts | 14 +- .../src/record/record-query.helper.ts | 17 +- .../src/record/record-select-field-visitor.ts | 20 +- .../persistence/src/record/record-utils.ts | 10 + .../src/record/record.mutate-visitor.ts | 35 +- .../src/record/record.query-repository.ts | 18 +- .../src/record/record.repository.ts | 13 +- packages/persistence/src/schema/postgres.ts | 424 ++++ .../src/{tables.ts => schema/sqlite.ts} | 0 packages/persistence/src/server.ts | 12 +- .../src/table/table.mutation-visitor.ts | 2 +- .../persistence/src/template/template-data.ts | 26 +- .../strategies/string-to-user.strategy.ts | 2 +- .../strategies/user-to-string.strategy.ts | 2 +- .../underlying-table-field-updated.visitor.ts | 9 +- .../underlying-table-field.visitor.ts | 57 +- .../underlying-table-spec.visitor.ts | 15 +- .../underlying/underlying-table.service.ts | 12 +- .../src/underlying/underlying-table.util.ts | 5 +- packages/persistence/src/utils/fn.util.ts | 32 + .../src/templates/everything.base.json | 14 + scripts/migrate.ts | 4 +- 79 files changed, 3226 insertions(+), 131 deletions(-) create mode 100644 apps/backend/drizzle/postgres/0000_low_jimmy_woo.sql create mode 100644 apps/backend/drizzle/postgres/meta/0000_snapshot.json create mode 100644 apps/backend/drizzle/postgres/meta/_journal.json rename apps/backend/drizzle/{ => sqlite}/0000_lively_warstar.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0001_familiar_joshua_kane.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0002_fixed_lockjaw.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0003_dry_starhawk.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0004_tricky_phil_sheldon.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0005_narrow_khan.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0006_mature_madame_web.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0007_steep_dragon_lord.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0008_bored_terror.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0009_workable_scorpion.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0010_nostalgic_nehzno.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0011_serious_marvex.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0012_lying_tomorrow_man.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0013_lovely_mordo.sql (100%) rename apps/backend/drizzle/{ => sqlite}/0014_messy_sasquatch.sql (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0000_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0001_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0002_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0003_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0004_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0005_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0006_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0007_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0008_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0009_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0010_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0011_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0012_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0013_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/0014_snapshot.json (100%) rename apps/backend/drizzle/{ => sqlite}/meta/_journal.json (100%) create mode 100644 drizzle.postgres.config.ts rename drizzle.config.ts => drizzle.sqlite.config.ts (66%) create mode 100644 packages/persistence/src/db.provider.ts create mode 100644 packages/persistence/src/schema/postgres.ts rename packages/persistence/src/{tables.ts => schema/sqlite.ts} (100%) create mode 100644 packages/persistence/src/utils/fn.util.ts diff --git a/apps/backend/drizzle/postgres/0000_low_jimmy_woo.sql b/apps/backend/drizzle/postgres/0000_low_jimmy_woo.sql new file mode 100644 index 000000000..8444fde99 --- /dev/null +++ b/apps/backend/drizzle/postgres/0000_low_jimmy_woo.sql @@ -0,0 +1,277 @@ +CREATE TABLE "undb_api_token" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "user_id" text NOT NULL, + "space_id" text NOT NULL, + "token" text NOT NULL, + CONSTRAINT "undb_api_token_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "undb_attachment_mapping" ( + "attachment_id" text NOT NULL, + "table_id" text NOT NULL, + "record_id" text NOT NULL, + "field_id" text NOT NULL, + CONSTRAINT "undb_attachment_mapping_attachment_id_table_id_record_id_field_id_pk" PRIMARY KEY("attachment_id","table_id","record_id","field_id") +); +--> statement-breakpoint +CREATE TABLE "undb_attachment" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "size" integer NOT NULL, + "mime_type" text NOT NULL, + "url" text NOT NULL, + "token" text, + "created_at" bigint NOT NULL, + "created_by" text NOT NULL, + "space_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "undb_audit" ( + "id" text PRIMARY KEY NOT NULL, + "timestamp" bigint NOT NULL, + "detail" jsonb, + "meta" jsonb, + "op" text NOT NULL, + "table_id" text NOT NULL, + "record_id" text NOT NULL, + "operator_id" text NOT NULL, + "space_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "undb_base" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "space_id" text NOT NULL, + "created_at" text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + "created_by" text NOT NULL, + "updated_at" text NOT NULL, + "updated_by" text NOT NULL, + CONSTRAINT "base_name_unique_idx" UNIQUE("name","space_id") +); +--> statement-breakpoint +CREATE TABLE "undb_dashboard_table_id_mapping" ( + "dashboard_id" text NOT NULL, + "table_id" text NOT NULL, + CONSTRAINT "undb_dashboard_table_id_mapping_dashboard_id_table_id_pk" PRIMARY KEY("dashboard_id","table_id") +); +--> statement-breakpoint +CREATE TABLE "undb_dashboard" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "description" text, + "base_id" text NOT NULL, + "space_id" text NOT NULL, + "widgets" jsonb, + "layout" jsonb, + "created_at" text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + "created_by" text NOT NULL, + "updated_at" text NOT NULL, + "updated_by" text NOT NULL, + CONSTRAINT "dashboard_name_unique_idx" UNIQUE("name","base_id") +); +--> statement-breakpoint +CREATE TABLE "undb_email_verification_code" ( + "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "undb_email_verification_code_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "code" text NOT NULL, + "user_id" text, + "email" text NOT NULL, + "expires_at" bigint NOT NULL, + CONSTRAINT "undb_email_verification_code_user_id_unique" UNIQUE("user_id") +); +--> statement-breakpoint +CREATE TABLE "undb_invitation" ( + "id" text PRIMARY KEY NOT NULL, + "email" text NOT NULL, + "role" text NOT NULL, + "status" text NOT NULL, + "space_id" text NOT NULL, + "invited_at" bigint NOT NULL, + "inviter_id" text NOT NULL, + CONSTRAINT "invitation_unique_idx" UNIQUE("email","space_id") +); +--> statement-breakpoint +CREATE TABLE "undb_oauth_account" ( + "provider_id" text NOT NULL, + "provider_user_id" text NOT NULL, + "user_id" text NOT NULL, + CONSTRAINT "undb_oauth_account_provider_id_provider_user_id_pk" PRIMARY KEY("provider_id","provider_user_id") +); +--> statement-breakpoint +CREATE TABLE "undb_outbox" ( + "id" text PRIMARY KEY NOT NULL, + "payload" jsonb NOT NULL, + "meta" jsonb, + "timestamp" bigint NOT NULL, + "user_id" text, + "name" text NOT NULL, + "space_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "undb_password_reset_token" ( + "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "undb_password_reset_token_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "token" text NOT NULL, + "user_id" text NOT NULL, + "expires_at" bigint NOT NULL, + CONSTRAINT "undb_password_reset_token_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "undb_reference_id_mapping" ( + "field_id" text NOT NULL, + "table_id" text NOT NULL, + "symmetric_field_id" text, + "foreign_table_id" text NOT NULL, + CONSTRAINT "reference_id_mapping_unique_idx" UNIQUE("field_id","table_id","symmetric_field_id","foreign_table_id") +); +--> statement-breakpoint +CREATE TABLE "undb_rollup_id_mapping" ( + "field_id" text NOT NULL, + "table_id" text NOT NULL, + "rollup_id" text NOT NULL, + "rollup_table_id" text NOT NULL, + CONSTRAINT "undb_rollup_id_mapping_field_id_rollup_id_pk" PRIMARY KEY("field_id","rollup_id") +); +--> statement-breakpoint +CREATE TABLE "undb_session" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "space_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "undb_share" ( + "id" text PRIMARY KEY NOT NULL, + "target_type" text NOT NULL, + "target_id" text NOT NULL, + "enabled" boolean NOT NULL, + "space_id" text NOT NULL, + CONSTRAINT "share_unique_idx" UNIQUE("target_type","target_id") +); +--> statement-breakpoint +CREATE TABLE "undb_space" ( + "id" text PRIMARY KEY NOT NULL, + "name" text, + "is_personal" boolean NOT NULL, + "avatar" text, + "created_at" text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + "created_by" text NOT NULL, + "updated_at" text NOT NULL, + "updated_by" text NOT NULL, + "deleted_at" bigint, + "deleted_by" text +); +--> statement-breakpoint +CREATE TABLE "undb_space_member" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "role" text NOT NULL, + "space_id" text NOT NULL, + CONSTRAINT "space_member_unique_idx" UNIQUE("user_id","space_id") +); +--> statement-breakpoint +CREATE TABLE "undb_table_id_mapping" ( + "table_id" text NOT NULL, + "subject_id" text NOT NULL, + CONSTRAINT "undb_table_id_mapping_table_id_subject_id_pk" PRIMARY KEY("table_id","subject_id") +); +--> statement-breakpoint +CREATE TABLE "undb_table" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "base_id" text NOT NULL, + "space_id" text NOT NULL, + "schema" jsonb NOT NULL, + "views" jsonb NOT NULL, + "forms" jsonb, + "rls" jsonb, + "widgets" jsonb, + "created_at" text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + "created_by" text NOT NULL, + "updated_at" text NOT NULL, + "updated_by" text NOT NULL, + CONSTRAINT "table_name_unique_idx" UNIQUE("name","base_id") +); +--> statement-breakpoint +CREATE TABLE "undb_user" ( + "id" text PRIMARY KEY NOT NULL, + "username" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "password" text NOT NULL, + "avatar" text, + "otp_secret" text, + CONSTRAINT "undb_user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "undb_webhook" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "url" text NOT NULL, + "method" text NOT NULL, + "enabled" boolean NOT NULL, + "table_id" text NOT NULL, + "headers" jsonb NOT NULL, + "condition" jsonb, + "event" text NOT NULL, + "space_id" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "undb_api_token" ADD CONSTRAINT "undb_api_token_user_id_undb_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_api_token" ADD CONSTRAINT "undb_api_token_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_attachment_mapping" ADD CONSTRAINT "undb_attachment_mapping_attachment_id_undb_attachment_id_fk" FOREIGN KEY ("attachment_id") REFERENCES "public"."undb_attachment"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_attachment_mapping" ADD CONSTRAINT "undb_attachment_mapping_table_id_undb_table_id_fk" FOREIGN KEY ("table_id") REFERENCES "public"."undb_table"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_attachment" ADD CONSTRAINT "undb_attachment_created_by_undb_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_attachment" ADD CONSTRAINT "undb_attachment_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_audit" ADD CONSTRAINT "undb_audit_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_base" ADD CONSTRAINT "undb_base_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_base" ADD CONSTRAINT "undb_base_created_by_undb_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_base" ADD CONSTRAINT "undb_base_updated_by_undb_user_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_dashboard_table_id_mapping" ADD CONSTRAINT "undb_dashboard_table_id_mapping_dashboard_id_undb_dashboard_id_fk" FOREIGN KEY ("dashboard_id") REFERENCES "public"."undb_dashboard"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_dashboard_table_id_mapping" ADD CONSTRAINT "undb_dashboard_table_id_mapping_table_id_undb_table_id_fk" FOREIGN KEY ("table_id") REFERENCES "public"."undb_table"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_dashboard" ADD CONSTRAINT "undb_dashboard_base_id_undb_base_id_fk" FOREIGN KEY ("base_id") REFERENCES "public"."undb_base"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_dashboard" ADD CONSTRAINT "undb_dashboard_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_dashboard" ADD CONSTRAINT "undb_dashboard_created_by_undb_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_dashboard" ADD CONSTRAINT "undb_dashboard_updated_by_undb_user_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_email_verification_code" ADD CONSTRAINT "undb_email_verification_code_user_id_undb_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_invitation" ADD CONSTRAINT "undb_invitation_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_invitation" ADD CONSTRAINT "undb_invitation_inviter_id_undb_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_oauth_account" ADD CONSTRAINT "undb_oauth_account_user_id_undb_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_outbox" ADD CONSTRAINT "undb_outbox_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_password_reset_token" ADD CONSTRAINT "undb_password_reset_token_user_id_undb_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_session" ADD CONSTRAINT "undb_session_user_id_undb_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_session" ADD CONSTRAINT "undb_session_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_share" ADD CONSTRAINT "undb_share_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_space" ADD CONSTRAINT "undb_space_created_by_undb_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_space" ADD CONSTRAINT "undb_space_updated_by_undb_user_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_space" ADD CONSTRAINT "undb_space_deleted_by_undb_user_id_fk" FOREIGN KEY ("deleted_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_space_member" ADD CONSTRAINT "undb_space_member_user_id_undb_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_space_member" ADD CONSTRAINT "undb_space_member_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_table_id_mapping" ADD CONSTRAINT "undb_table_id_mapping_table_id_undb_table_id_fk" FOREIGN KEY ("table_id") REFERENCES "public"."undb_table"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_table" ADD CONSTRAINT "undb_table_base_id_undb_base_id_fk" FOREIGN KEY ("base_id") REFERENCES "public"."undb_base"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_table" ADD CONSTRAINT "undb_table_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_table" ADD CONSTRAINT "undb_table_created_by_undb_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_table" ADD CONSTRAINT "undb_table_updated_by_undb_user_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."undb_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_webhook" ADD CONSTRAINT "undb_webhook_table_id_undb_table_id_fk" FOREIGN KEY ("table_id") REFERENCES "public"."undb_table"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "undb_webhook" ADD CONSTRAINT "undb_webhook_space_id_undb_space_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."undb_space"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "api_token_space_id_idx" ON "undb_api_token" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "api_token_user_id_idx" ON "undb_api_token" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "attachment_size_idx" ON "undb_attachment" USING btree ("size");--> statement-breakpoint +CREATE INDEX "attachment_space_id_idx" ON "undb_attachment" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "audit_table_id_idx" ON "undb_audit" USING btree ("table_id");--> statement-breakpoint +CREATE INDEX "audit_space_id_idx" ON "undb_audit" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "audit_record_id_idx" ON "undb_audit" USING btree ("record_id");--> statement-breakpoint +CREATE INDEX "base_space_id_idx" ON "undb_base" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "dashboard_base_id_idx" ON "undb_dashboard" USING btree ("base_id");--> statement-breakpoint +CREATE INDEX "dashboard_space_id_idx" ON "undb_dashboard" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "invitation_space_id_idx" ON "undb_invitation" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "outbox_space_id_idx" ON "undb_outbox" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "password_reset_token_user_id_idx" ON "undb_password_reset_token" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "share_space_id_idx" ON "undb_share" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "space_name_idx" ON "undb_space" USING btree ("name");--> statement-breakpoint +CREATE INDEX "table_base_id_idx" ON "undb_table" USING btree ("base_id");--> statement-breakpoint +CREATE INDEX "table_space_id_idx" ON "undb_table" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "user_username_idx" ON "undb_user" USING btree ("username");--> statement-breakpoint +CREATE INDEX "user_email_idx" ON "undb_user" USING btree ("email");--> statement-breakpoint +CREATE INDEX "webhook_table_id_idx" ON "undb_webhook" USING btree ("table_id");--> statement-breakpoint +CREATE INDEX "webhook_space_id_idx" ON "undb_webhook" USING btree ("space_id");--> statement-breakpoint +CREATE INDEX "webhook_url_idx" ON "undb_webhook" USING btree ("url"); \ No newline at end of file diff --git a/apps/backend/drizzle/postgres/meta/0000_snapshot.json b/apps/backend/drizzle/postgres/meta/0000_snapshot.json new file mode 100644 index 000000000..33b4cc7a2 --- /dev/null +++ b/apps/backend/drizzle/postgres/meta/0000_snapshot.json @@ -0,0 +1,2130 @@ +{ + "id": "40cb0778-9031-4755-84c9-b94ad5f09718", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.undb_api_token": { + "name": "undb_api_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "api_token_space_id_idx": { + "name": "api_token_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_token_user_id_idx": { + "name": "api_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_api_token_user_id_undb_user_id_fk": { + "name": "undb_api_token_user_id_undb_user_id_fk", + "tableFrom": "undb_api_token", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_api_token_space_id_undb_space_id_fk": { + "name": "undb_api_token_space_id_undb_space_id_fk", + "tableFrom": "undb_api_token", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "undb_api_token_token_unique": { + "name": "undb_api_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_attachment_mapping": { + "name": "undb_attachment_mapping", + "schema": "", + "columns": { + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "undb_attachment_mapping_attachment_id_undb_attachment_id_fk": { + "name": "undb_attachment_mapping_attachment_id_undb_attachment_id_fk", + "tableFrom": "undb_attachment_mapping", + "tableTo": "undb_attachment", + "columnsFrom": [ + "attachment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_attachment_mapping_table_id_undb_table_id_fk": { + "name": "undb_attachment_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_attachment_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_attachment_mapping_attachment_id_table_id_record_id_field_id_pk": { + "name": "undb_attachment_mapping_attachment_id_table_id_record_id_field_id_pk", + "columns": [ + "attachment_id", + "table_id", + "record_id", + "field_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_attachment": { + "name": "undb_attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "attachment_size_idx": { + "name": "attachment_size_idx", + "columns": [ + { + "expression": "size", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachment_space_id_idx": { + "name": "attachment_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_attachment_created_by_undb_user_id_fk": { + "name": "undb_attachment_created_by_undb_user_id_fk", + "tableFrom": "undb_attachment", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_attachment_space_id_undb_space_id_fk": { + "name": "undb_attachment_space_id_undb_space_id_fk", + "tableFrom": "undb_attachment", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_audit": { + "name": "undb_audit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "op": { + "name": "op", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operator_id": { + "name": "operator_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "audit_table_id_idx": { + "name": "audit_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_space_id_idx": { + "name": "audit_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_record_id_idx": { + "name": "audit_record_id_idx", + "columns": [ + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_audit_space_id_undb_space_id_fk": { + "name": "undb_audit_space_id_undb_space_id_fk", + "tableFrom": "undb_audit", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_base": { + "name": "undb_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "base_space_id_idx": { + "name": "base_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_base_space_id_undb_space_id_fk": { + "name": "undb_base_space_id_undb_space_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_base_created_by_undb_user_id_fk": { + "name": "undb_base_created_by_undb_user_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_base_updated_by_undb_user_id_fk": { + "name": "undb_base_updated_by_undb_user_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "base_name_unique_idx": { + "name": "base_name_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "name", + "space_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_dashboard_table_id_mapping": { + "name": "undb_dashboard_table_id_mapping", + "schema": "", + "columns": { + "dashboard_id": { + "name": "dashboard_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "undb_dashboard_table_id_mapping_dashboard_id_undb_dashboard_id_fk": { + "name": "undb_dashboard_table_id_mapping_dashboard_id_undb_dashboard_id_fk", + "tableFrom": "undb_dashboard_table_id_mapping", + "tableTo": "undb_dashboard", + "columnsFrom": [ + "dashboard_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_table_id_mapping_table_id_undb_table_id_fk": { + "name": "undb_dashboard_table_id_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_dashboard_table_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_dashboard_table_id_mapping_dashboard_id_table_id_pk": { + "name": "undb_dashboard_table_id_mapping_dashboard_id_table_id_pk", + "columns": [ + "dashboard_id", + "table_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_dashboard": { + "name": "undb_dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_id": { + "name": "base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "widgets": { + "name": "widgets", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dashboard_base_id_idx": { + "name": "dashboard_base_id_idx", + "columns": [ + { + "expression": "base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dashboard_space_id_idx": { + "name": "dashboard_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_dashboard_base_id_undb_base_id_fk": { + "name": "undb_dashboard_base_id_undb_base_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_base", + "columnsFrom": [ + "base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_space_id_undb_space_id_fk": { + "name": "undb_dashboard_space_id_undb_space_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_created_by_undb_user_id_fk": { + "name": "undb_dashboard_created_by_undb_user_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_updated_by_undb_user_id_fk": { + "name": "undb_dashboard_updated_by_undb_user_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_name_unique_idx": { + "name": "dashboard_name_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "name", + "base_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_email_verification_code": { + "name": "undb_email_verification_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "undb_email_verification_code_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "undb_email_verification_code_user_id_undb_user_id_fk": { + "name": "undb_email_verification_code_user_id_undb_user_id_fk", + "tableFrom": "undb_email_verification_code", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "undb_email_verification_code_user_id_unique": { + "name": "undb_email_verification_code_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_invitation": { + "name": "undb_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_space_id_idx": { + "name": "invitation_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_invitation_space_id_undb_space_id_fk": { + "name": "undb_invitation_space_id_undb_space_id_fk", + "tableFrom": "undb_invitation", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_invitation_inviter_id_undb_user_id_fk": { + "name": "undb_invitation_inviter_id_undb_user_id_fk", + "tableFrom": "undb_invitation", + "tableTo": "undb_user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_unique_idx": { + "name": "invitation_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "email", + "space_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_oauth_account": { + "name": "undb_oauth_account", + "schema": "", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "undb_oauth_account_user_id_undb_user_id_fk": { + "name": "undb_oauth_account_user_id_undb_user_id_fk", + "tableFrom": "undb_oauth_account", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_oauth_account_provider_id_provider_user_id_pk": { + "name": "undb_oauth_account_provider_id_provider_user_id_pk", + "columns": [ + "provider_id", + "provider_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_outbox": { + "name": "undb_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "outbox_space_id_idx": { + "name": "outbox_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_outbox_space_id_undb_space_id_fk": { + "name": "undb_outbox_space_id_undb_space_id_fk", + "tableFrom": "undb_outbox", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_password_reset_token": { + "name": "undb_password_reset_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "undb_password_reset_token_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "password_reset_token_user_id_idx": { + "name": "password_reset_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_password_reset_token_user_id_undb_user_id_fk": { + "name": "undb_password_reset_token_user_id_undb_user_id_fk", + "tableFrom": "undb_password_reset_token", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "undb_password_reset_token_token_unique": { + "name": "undb_password_reset_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_reference_id_mapping": { + "name": "undb_reference_id_mapping", + "schema": "", + "columns": { + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "symmetric_field_id": { + "name": "symmetric_field_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "foreign_table_id": { + "name": "foreign_table_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reference_id_mapping_unique_idx": { + "name": "reference_id_mapping_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "field_id", + "table_id", + "symmetric_field_id", + "foreign_table_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_rollup_id_mapping": { + "name": "undb_rollup_id_mapping", + "schema": "", + "columns": { + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rollup_id": { + "name": "rollup_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rollup_table_id": { + "name": "rollup_table_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "undb_rollup_id_mapping_field_id_rollup_id_pk": { + "name": "undb_rollup_id_mapping_field_id_rollup_id_pk", + "columns": [ + "field_id", + "rollup_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_session": { + "name": "undb_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "undb_session_user_id_undb_user_id_fk": { + "name": "undb_session_user_id_undb_user_id_fk", + "tableFrom": "undb_session", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_session_space_id_undb_space_id_fk": { + "name": "undb_session_space_id_undb_space_id_fk", + "tableFrom": "undb_session", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_share": { + "name": "undb_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "share_space_id_idx": { + "name": "share_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_share_space_id_undb_space_id_fk": { + "name": "undb_share_space_id_undb_space_id_fk", + "tableFrom": "undb_share", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "share_unique_idx": { + "name": "share_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "target_type", + "target_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_space": { + "name": "undb_space", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_personal": { + "name": "is_personal", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deleted_by": { + "name": "deleted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "space_name_idx": { + "name": "space_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_space_created_by_undb_user_id_fk": { + "name": "undb_space_created_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_updated_by_undb_user_id_fk": { + "name": "undb_space_updated_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_deleted_by_undb_user_id_fk": { + "name": "undb_space_deleted_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "deleted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_space_member": { + "name": "undb_space_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "undb_space_member_user_id_undb_user_id_fk": { + "name": "undb_space_member_user_id_undb_user_id_fk", + "tableFrom": "undb_space_member", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_member_space_id_undb_space_id_fk": { + "name": "undb_space_member_space_id_undb_space_id_fk", + "tableFrom": "undb_space_member", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "space_member_unique_idx": { + "name": "space_member_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "space_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_table_id_mapping": { + "name": "undb_table_id_mapping", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "undb_table_id_mapping_table_id_undb_table_id_fk": { + "name": "undb_table_id_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_table_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_table_id_mapping_table_id_subject_id_pk": { + "name": "undb_table_id_mapping_table_id_subject_id_pk", + "columns": [ + "table_id", + "subject_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_table": { + "name": "undb_table", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_id": { + "name": "base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "forms": { + "name": "forms", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rls": { + "name": "rls", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "widgets": { + "name": "widgets", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "table_base_id_idx": { + "name": "table_base_id_idx", + "columns": [ + { + "expression": "base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_space_id_idx": { + "name": "table_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_table_base_id_undb_base_id_fk": { + "name": "undb_table_base_id_undb_base_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_base", + "columnsFrom": [ + "base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_space_id_undb_space_id_fk": { + "name": "undb_table_space_id_undb_space_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_created_by_undb_user_id_fk": { + "name": "undb_table_created_by_undb_user_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_updated_by_undb_user_id_fk": { + "name": "undb_table_updated_by_undb_user_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "table_name_unique_idx": { + "name": "table_name_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "name", + "base_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_user": { + "name": "undb_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "otp_secret": { + "name": "otp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_username_idx": { + "name": "user_username_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_email_idx": { + "name": "user_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "undb_user_email_unique": { + "name": "undb_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.undb_webhook": { + "name": "undb_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "condition": { + "name": "condition", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "webhook_table_id_idx": { + "name": "webhook_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_space_id_idx": { + "name": "webhook_space_id_idx", + "columns": [ + { + "expression": "space_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_url_idx": { + "name": "webhook_url_idx", + "columns": [ + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "undb_webhook_table_id_undb_table_id_fk": { + "name": "undb_webhook_table_id_undb_table_id_fk", + "tableFrom": "undb_webhook", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_webhook_space_id_undb_space_id_fk": { + "name": "undb_webhook_space_id_undb_space_id_fk", + "tableFrom": "undb_webhook", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/postgres/meta/_journal.json b/apps/backend/drizzle/postgres/meta/_journal.json new file mode 100644 index 000000000..d1f4240c9 --- /dev/null +++ b/apps/backend/drizzle/postgres/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1736487832119, + "tag": "0000_low_jimmy_woo", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/backend/drizzle/0000_lively_warstar.sql b/apps/backend/drizzle/sqlite/0000_lively_warstar.sql similarity index 100% rename from apps/backend/drizzle/0000_lively_warstar.sql rename to apps/backend/drizzle/sqlite/0000_lively_warstar.sql diff --git a/apps/backend/drizzle/0001_familiar_joshua_kane.sql b/apps/backend/drizzle/sqlite/0001_familiar_joshua_kane.sql similarity index 100% rename from apps/backend/drizzle/0001_familiar_joshua_kane.sql rename to apps/backend/drizzle/sqlite/0001_familiar_joshua_kane.sql diff --git a/apps/backend/drizzle/0002_fixed_lockjaw.sql b/apps/backend/drizzle/sqlite/0002_fixed_lockjaw.sql similarity index 100% rename from apps/backend/drizzle/0002_fixed_lockjaw.sql rename to apps/backend/drizzle/sqlite/0002_fixed_lockjaw.sql diff --git a/apps/backend/drizzle/0003_dry_starhawk.sql b/apps/backend/drizzle/sqlite/0003_dry_starhawk.sql similarity index 100% rename from apps/backend/drizzle/0003_dry_starhawk.sql rename to apps/backend/drizzle/sqlite/0003_dry_starhawk.sql diff --git a/apps/backend/drizzle/0004_tricky_phil_sheldon.sql b/apps/backend/drizzle/sqlite/0004_tricky_phil_sheldon.sql similarity index 100% rename from apps/backend/drizzle/0004_tricky_phil_sheldon.sql rename to apps/backend/drizzle/sqlite/0004_tricky_phil_sheldon.sql diff --git a/apps/backend/drizzle/0005_narrow_khan.sql b/apps/backend/drizzle/sqlite/0005_narrow_khan.sql similarity index 100% rename from apps/backend/drizzle/0005_narrow_khan.sql rename to apps/backend/drizzle/sqlite/0005_narrow_khan.sql diff --git a/apps/backend/drizzle/0006_mature_madame_web.sql b/apps/backend/drizzle/sqlite/0006_mature_madame_web.sql similarity index 100% rename from apps/backend/drizzle/0006_mature_madame_web.sql rename to apps/backend/drizzle/sqlite/0006_mature_madame_web.sql diff --git a/apps/backend/drizzle/0007_steep_dragon_lord.sql b/apps/backend/drizzle/sqlite/0007_steep_dragon_lord.sql similarity index 100% rename from apps/backend/drizzle/0007_steep_dragon_lord.sql rename to apps/backend/drizzle/sqlite/0007_steep_dragon_lord.sql diff --git a/apps/backend/drizzle/0008_bored_terror.sql b/apps/backend/drizzle/sqlite/0008_bored_terror.sql similarity index 100% rename from apps/backend/drizzle/0008_bored_terror.sql rename to apps/backend/drizzle/sqlite/0008_bored_terror.sql diff --git a/apps/backend/drizzle/0009_workable_scorpion.sql b/apps/backend/drizzle/sqlite/0009_workable_scorpion.sql similarity index 100% rename from apps/backend/drizzle/0009_workable_scorpion.sql rename to apps/backend/drizzle/sqlite/0009_workable_scorpion.sql diff --git a/apps/backend/drizzle/0010_nostalgic_nehzno.sql b/apps/backend/drizzle/sqlite/0010_nostalgic_nehzno.sql similarity index 100% rename from apps/backend/drizzle/0010_nostalgic_nehzno.sql rename to apps/backend/drizzle/sqlite/0010_nostalgic_nehzno.sql diff --git a/apps/backend/drizzle/0011_serious_marvex.sql b/apps/backend/drizzle/sqlite/0011_serious_marvex.sql similarity index 100% rename from apps/backend/drizzle/0011_serious_marvex.sql rename to apps/backend/drizzle/sqlite/0011_serious_marvex.sql diff --git a/apps/backend/drizzle/0012_lying_tomorrow_man.sql b/apps/backend/drizzle/sqlite/0012_lying_tomorrow_man.sql similarity index 100% rename from apps/backend/drizzle/0012_lying_tomorrow_man.sql rename to apps/backend/drizzle/sqlite/0012_lying_tomorrow_man.sql diff --git a/apps/backend/drizzle/0013_lovely_mordo.sql b/apps/backend/drizzle/sqlite/0013_lovely_mordo.sql similarity index 100% rename from apps/backend/drizzle/0013_lovely_mordo.sql rename to apps/backend/drizzle/sqlite/0013_lovely_mordo.sql diff --git a/apps/backend/drizzle/0014_messy_sasquatch.sql b/apps/backend/drizzle/sqlite/0014_messy_sasquatch.sql similarity index 100% rename from apps/backend/drizzle/0014_messy_sasquatch.sql rename to apps/backend/drizzle/sqlite/0014_messy_sasquatch.sql diff --git a/apps/backend/drizzle/meta/0000_snapshot.json b/apps/backend/drizzle/sqlite/meta/0000_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0000_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0000_snapshot.json diff --git a/apps/backend/drizzle/meta/0001_snapshot.json b/apps/backend/drizzle/sqlite/meta/0001_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0001_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0001_snapshot.json diff --git a/apps/backend/drizzle/meta/0002_snapshot.json b/apps/backend/drizzle/sqlite/meta/0002_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0002_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0002_snapshot.json diff --git a/apps/backend/drizzle/meta/0003_snapshot.json b/apps/backend/drizzle/sqlite/meta/0003_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0003_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0003_snapshot.json diff --git a/apps/backend/drizzle/meta/0004_snapshot.json b/apps/backend/drizzle/sqlite/meta/0004_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0004_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0004_snapshot.json diff --git a/apps/backend/drizzle/meta/0005_snapshot.json b/apps/backend/drizzle/sqlite/meta/0005_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0005_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0005_snapshot.json diff --git a/apps/backend/drizzle/meta/0006_snapshot.json b/apps/backend/drizzle/sqlite/meta/0006_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0006_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0006_snapshot.json diff --git a/apps/backend/drizzle/meta/0007_snapshot.json b/apps/backend/drizzle/sqlite/meta/0007_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0007_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0007_snapshot.json diff --git a/apps/backend/drizzle/meta/0008_snapshot.json b/apps/backend/drizzle/sqlite/meta/0008_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0008_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0008_snapshot.json diff --git a/apps/backend/drizzle/meta/0009_snapshot.json b/apps/backend/drizzle/sqlite/meta/0009_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0009_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0009_snapshot.json diff --git a/apps/backend/drizzle/meta/0010_snapshot.json b/apps/backend/drizzle/sqlite/meta/0010_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0010_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0010_snapshot.json diff --git a/apps/backend/drizzle/meta/0011_snapshot.json b/apps/backend/drizzle/sqlite/meta/0011_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0011_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0011_snapshot.json diff --git a/apps/backend/drizzle/meta/0012_snapshot.json b/apps/backend/drizzle/sqlite/meta/0012_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0012_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0012_snapshot.json diff --git a/apps/backend/drizzle/meta/0013_snapshot.json b/apps/backend/drizzle/sqlite/meta/0013_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0013_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0013_snapshot.json diff --git a/apps/backend/drizzle/meta/0014_snapshot.json b/apps/backend/drizzle/sqlite/meta/0014_snapshot.json similarity index 100% rename from apps/backend/drizzle/meta/0014_snapshot.json rename to apps/backend/drizzle/sqlite/meta/0014_snapshot.json diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/sqlite/meta/_journal.json similarity index 100% rename from apps/backend/drizzle/meta/_journal.json rename to apps/backend/drizzle/sqlite/meta/_journal.json diff --git a/apps/backend/package.json b/apps/backend/package.json index 643e736e6..c8c0b99a4 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -20,8 +20,11 @@ "@elysiajs/trpc": "^1.1.0", "@json2csv/plainjs": "^7.0.6", "@kitajs/ts-html-plugin": "latest", + "@lucia-auth/adapter-drizzle": "^1.1.0", + "@lucia-auth/adapter-postgresql": "^3.1.2", "@lucia-auth/adapter-sqlite": "^3.0.2", "@oslojs/otp": "^1.1.0", + "@types/pg": "^8.11.10", "@undb/audit": "workspace:*", "@undb/authz": "workspace:*", "@undb/base": "workspace:*", @@ -54,6 +57,7 @@ "nanoid": "^5.0.9", "nodemailer": "^6.9.16", "oslo": "^1.2.1", + "pg": "^8.13.1", "radash": "^12.1.0", "reflect-metadata": "^0.2.2", "uuid": "^11.0.5", diff --git a/apps/backend/src/modules/auth/auth.provider.ts b/apps/backend/src/modules/auth/auth.provider.ts index 1c199865b..090a4693a 100644 --- a/apps/backend/src/modules/auth/auth.provider.ts +++ b/apps/backend/src/modules/auth/auth.provider.ts @@ -1,9 +1,11 @@ +import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle" import { BunSQLiteAdapter, LibSQLAdapter } from "@lucia-auth/adapter-sqlite" import { container, inject, instanceCachingFactory } from "@undb/di" -import { env } from "@undb/env" -import { Client, DATABASE_CLIENT } from "@undb/persistence/server" +import { Client, DATABASE_CLIENT, DB_PROVIDER, pgSessionTable, pgUsers } from "@undb/persistence/server" import Database from "bun:sqlite" +import { drizzle } from "drizzle-orm/node-postgres" import { Adapter, Lucia } from "lucia" +import pg from "pg" export const LUCIA_PROVIDER = Symbol.for("LUCIA_PROVIDER") export const injectLucia = () => inject(LUCIA_PROVIDER) @@ -19,12 +21,12 @@ const createLuciaWithAdapter = (adapter: Adapter) => { }, getSessionAttributes: (attributes) => { return { - spaceId: attributes.space_id, + spaceId: attributes.space_id ?? attributes.spaceId, } }, getUserAttributes: (attributes) => { return { - emailVerified: Boolean(attributes.email_verified), + emailVerified: Boolean(attributes.email_verified || attributes.emailVerified), email: attributes.email, username: attributes.username, avatar: attributes.avatar, @@ -53,6 +55,14 @@ const createSqliteLucia = (sqlite: Database) => { return createLuciaWithAdapter(adapter) } +const createPostgresLucia = (pool: pg.Pool) => { + const db = drizzle(pool) + + const adapter = new DrizzlePostgreSQLAdapter(db, pgSessionTable, pgUsers) + + return createLuciaWithAdapter(adapter) +} + declare module "lucia" { interface Register { Lucia: ReturnType @@ -60,21 +70,28 @@ declare module "lucia" { DatabaseUserAttributes: { email: string email_verified: boolean + emailVerified: boolean username: string avatar?: string } } interface DatabaseSessionAttributes { space_id: string + spaceId: string } } container.register(LUCIA_PROVIDER, { useFactory: instanceCachingFactory((c) => { - if (env.UNDB_DB_PROVIDER === "sqlite" || !env.UNDB_DB_PROVIDER) { + const dbProvider = c.resolve(DB_PROVIDER) + if (dbProvider === "sqlite" || !dbProvider) { const sqlite = c.resolve(DATABASE_CLIENT) return createSqliteLucia(sqlite) + } else if (dbProvider === "postgres") { + const pool = c.resolve(DATABASE_CLIENT) + return createPostgresLucia(pool) } + const sqlite = c.resolve(DATABASE_CLIENT) return createTursoLucia(sqlite) }), diff --git a/apps/backend/src/modules/auth/auth.ts b/apps/backend/src/modules/auth/auth.ts index e1e485bf5..ff3e1de83 100644 --- a/apps/backend/src/modules/auth/auth.ts +++ b/apps/backend/src/modules/auth/auth.ts @@ -379,7 +379,7 @@ export class Auth { }) } - const session = await this.lucia.createSession(userId, { space_id: spaceId }) + const session = await this.lucia.createSession(userId, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) response.headers.set("Set-Cookie", sessionCookie.serialize()) @@ -434,7 +434,8 @@ export class Auth { space = Some(await this.spaceService.createSpace({ name: user.username })) await this.spaceMemberService.createMember(user.id, space.unwrap().id.value, "owner") } - const session = await this.lucia.createSession(user.id, { space_id: space.unwrap().id.value }) + const spaceId = space.unwrap().id.value + const session = await this.lucia.createSession(user.id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { @@ -548,7 +549,8 @@ export class Auth { "User should have a space", ) - const session = await this.lucia.createSession(token.user_id, { space_id: space.id.value }) + const spaceId = space.id.value + const session = await this.lucia.createSession(token.user_id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { status: 302, @@ -609,7 +611,8 @@ export class Auth { .where("id", "=", user.id) .execute() - const session = await this.lucia.createSession(user.id, { space_id: validatedSession.spaceId }) + const spaceId = validatedSession.spaceId + const session = await this.lucia.createSession(user.id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { status: 302, @@ -666,7 +669,7 @@ export class Auth { return redirectToSignupOrLogin("signup", email) } else { await this.spaceMemberService.createMember(user.id, spaceId, role) - const session = await this.lucia.createSession(user.id, { space_id: spaceId }) + const session = await this.lucia.createSession(user.id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return redirectToSignupOrLogin("login", email, sessionCookie) } diff --git a/apps/backend/src/modules/auth/oauth/github.ts b/apps/backend/src/modules/auth/oauth/github.ts index 8b3d234d1..aaf24eab8 100644 --- a/apps/backend/src/modules/auth/oauth/github.ts +++ b/apps/backend/src/modules/auth/oauth/github.ts @@ -88,7 +88,8 @@ export class GithubOAuth { if (existingUser) { const space = (await this.spaceService.getSpace({ userId: existingUser.user_id })).expect("space not found") - const session = await this.lucia.createSession(existingUser.user_id, { space_id: space.id.value }) + const spaceId = space.id.value + const session = await this.lucia.createSession(existingUser.user_id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { status: 302, @@ -136,7 +137,8 @@ export class GithubOAuth { }) .execute() - const session = await this.lucia.createSession(existingGithubUser.id, { space_id: space.id.value }) + const spaceId = space.id.value + const session = await this.lucia.createSession(existingGithubUser.id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { status: 302, @@ -182,7 +184,9 @@ export class GithubOAuth { return space }) - const session = await this.lucia.createSession(userId, { space_id: space.id.value }) + + const spaceId = space.id.value + const session = await this.lucia.createSession(userId, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { status: 302, diff --git a/apps/backend/src/modules/auth/oauth/google.ts b/apps/backend/src/modules/auth/oauth/google.ts index b42df1ded..1e305f2fa 100644 --- a/apps/backend/src/modules/auth/oauth/google.ts +++ b/apps/backend/src/modules/auth/oauth/google.ts @@ -96,7 +96,9 @@ export class GoogleOAuth { if (existingUser) { const space = (await this.spaceService.getSpace({ userId: existingUser.user_id })).expect("Space not found") - const session = await this.lucia.createSession(existingUser.user_id, { space_id: space.id.value }) + + const spaceId = space.id.value + const session = await this.lucia.createSession(existingUser.user_id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { status: 302, @@ -124,7 +126,8 @@ export class GoogleOAuth { }) .execute() - const session = await this.lucia.createSession(existingGoogleUser.id, { space_id: space.id.value }) + const spaceId = space.id.value + const session = await this.lucia.createSession(existingGoogleUser.id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { status: 302, @@ -170,7 +173,8 @@ export class GoogleOAuth { return space }) - const session = await this.lucia.createSession(userId, { space_id: space.id.value }) + const spaceId = space.id.value + const session = await this.lucia.createSession(userId, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return new Response(null, { status: 302, diff --git a/apps/backend/src/modules/auth/otp.service.ts b/apps/backend/src/modules/auth/otp.service.ts index 53f0100b3..974033d53 100644 --- a/apps/backend/src/modules/auth/otp.service.ts +++ b/apps/backend/src/modules/auth/otp.service.ts @@ -107,7 +107,8 @@ export class OtpService implements IOtpService { await this.qb.updateTable("undb_user").set({ otp_secret: null }).where("undb_user.email", "=", email).execute() - const session = await this.lucia.createSession(id, { space_id: space.unwrap().id.value }) + const spaceId = space.unwrap().id.value + const session = await this.lucia.createSession(id, { space_id: spaceId, spaceId }) const sessionCookie = this.lucia.createSessionCookie(session.id) return sessionCookie.serialize() diff --git a/apps/backend/src/modules/space/space.module.ts b/apps/backend/src/modules/space/space.module.ts index e6fddc5f0..759edbca3 100644 --- a/apps/backend/src/modules/space/space.module.ts +++ b/apps/backend/src/modules/space/space.module.ts @@ -47,7 +47,8 @@ export class SpaceModule { }) } await this.lucia.invalidateUserSessions(user.id) - const updatedSession = await this.lucia.createSession(user.id, { space_id: space.id.value }) + const sid = space.id.value + const updatedSession = await this.lucia.createSession(user.id, { space_id: sid, spaceId: sid }) const sessionCookie = this.lucia.createSessionCookie(updatedSession.id) return new Response(null, { status: 302, @@ -74,7 +75,8 @@ export class SpaceModule { await this.lucia.invalidateSession(userId) const space = (await this.spaceService.getSpace({ userId })).expect("Space not found") - const updatedSession = await this.lucia.createSession(userId, { space_id: space.id.value }) + const sid = space.id.value + const updatedSession = await this.lucia.createSession(userId, { space_id: sid, spaceId: sid }) const sessionCookie = this.lucia.createSessionCookie(updatedSession.id) return new Response(null, { status: 200, diff --git a/apps/backend/src/registry/db.registry.ts b/apps/backend/src/registry/db.registry.ts index 9323badeb..c794431db 100644 --- a/apps/backend/src/registry/db.registry.ts +++ b/apps/backend/src/registry/db.registry.ts @@ -23,6 +23,8 @@ import { BaseQueryRepository, BaseRepository, Client, + createPostgresClient, + createPostgresQueryBuilder, createSqliteClient, createSqliteQueryBuilder, createTursoClient, @@ -32,6 +34,7 @@ import { DashboardQueryRepository, DashboardRepository, DATABASE_CLIENT, + DB_PROVIDER, InvitationQueryRepository, InvitationRepository, QUERY_BUILDER, @@ -71,31 +74,41 @@ import { USER_QUERY_REPOSITORY, USER_REPOSITORY, USER_SERVICE, UserService } fro import { WEBHOOK_QUERY_REPOSITORY, WEBHOOK_REPOSITORY } from "@undb/webhook" import Database from "bun:sqlite" import { AsyncLocalStorage } from "node:async_hooks" +import pg from "pg" const txContext = new AsyncLocalStorage() export const registerDb = () => { + container.register(DB_PROVIDER, { useValue: env.UNDB_DB_PROVIDER || "sqlite" }) container.register(CTX, { useValue: txContext }) - container.register(TX_CTX, TxContextImpl) - container.register(DATABASE_CLIENT, { useFactory: instanceCachingFactory(() => { - if (env.UNDB_DB_PROVIDER === "sqlite" || !env.UNDB_DB_PROVIDER) { + const dbProvider = container.resolve(DB_PROVIDER) + if (dbProvider === "sqlite" || !dbProvider) { return createSqliteClient("undb.sqlite") } + if (dbProvider === "postgres") { + return createPostgresClient(env.UNDB_DB_POSTGRES_URL!) + } return createTursoClient(env.UNDB_DB_TURSO_URL!, env.UNDB_DB_TURSO_AUTH_TOKEN) }), }) container.register(QUERY_BUILDER, { useFactory: instanceCachingFactory((c) => { - if (env.UNDB_DB_PROVIDER === "sqlite" || !env.UNDB_DB_PROVIDER) { + const dbProvider = container.resolve(DB_PROVIDER) + if (dbProvider === "sqlite" || !dbProvider) { const sqlite = c.resolve(DATABASE_CLIENT) return createSqliteQueryBuilder(sqlite) + } else if (dbProvider === "postgres") { + const pg = c.resolve(DATABASE_CLIENT) + return createPostgresQueryBuilder(pg) } + const sqlite = c.resolve(DATABASE_CLIENT) return createTursoQueryBuilder(sqlite) }), }) + container.register(TX_CTX, TxContextImpl) container.register(CONTEXT_TOKEN, ServerContext) container.register(SPACE_REPOSITORY, SpaceRepostitory) diff --git a/apps/frontend/src/lib/registry.svelte.ts b/apps/frontend/src/lib/registry.svelte.ts index 8e6fb8198..51078228d 100644 --- a/apps/frontend/src/lib/registry.svelte.ts +++ b/apps/frontend/src/lib/registry.svelte.ts @@ -1,4 +1,4 @@ -import { ISpaceMemberService,SPACE_MEMBER_SERVICE } from "@undb/authz" +import { ISpaceMemberService, SPACE_MEMBER_SERVICE } from "@undb/authz" import { AddDashboardWidgetCommandHandler, BulkDeleteRecordsCommandHandler, @@ -27,10 +27,11 @@ import { UpdateTableFieldCommandHandler, UpdateViewCommandHandler, } from "@undb/command-handlers" -import { CONTEXT_TOKEN,IContext } from "@undb/context" -import { CommandBus,QueryBus } from "@undb/cqrs" -import { DataService,registerDataService,TRPC_CLIENT } from "@undb/data-service" +import { CONTEXT_TOKEN, IContext } from "@undb/context" +import { CommandBus, QueryBus } from "@undb/cqrs" +import { DataService, registerDataService, TRPC_CLIENT } from "@undb/data-service" import { container } from "@undb/di" +import { DB_PROVIDER } from "@undb/persistence/client" import { GetAggregatesQueryHandler, GetBaseQueryHandler, @@ -91,6 +92,7 @@ export class Registry { async register(isLocal: boolean, isPlayground: boolean): Promise { const childContainer = container.createChildContainer() + childContainer.register(DB_PROVIDER, { useValue: "sqlite" }) childContainer.register(TRPC_CLIENT, { useValue: trpc }) await registerDataService(childContainer, isLocal, isPlayground) diff --git a/bun.lockb b/bun.lockb index 87118dee24db8e837e79ef3d18fcbe9824134b86..8b2cfdae05796b90fd65d7c9121b71fa97d6c005 100755 GIT binary patch delta 106242 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 Cs(@_( diff --git a/drizzle.postgres.config.ts b/drizzle.postgres.config.ts new file mode 100644 index 000000000..94d6ce140 --- /dev/null +++ b/drizzle.postgres.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "drizzle-kit" + +export default defineConfig({ + schema: "./packages/persistence/src/schema/postgres.ts", + out: "./apps/backend/drizzle/postgres", + dialect: "postgresql", + tablesFilter: ["undb_*"], +}) diff --git a/drizzle.config.ts b/drizzle.sqlite.config.ts similarity index 66% rename from drizzle.config.ts rename to drizzle.sqlite.config.ts index d14213605..b6528b4e6 100644 --- a/drizzle.config.ts +++ b/drizzle.sqlite.config.ts @@ -1,8 +1,8 @@ import { defineConfig } from "drizzle-kit" export default defineConfig({ - schema: "./packages/persistence/src/tables.ts", - out: "./apps/backend/drizzle", + schema: "./packages/persistence/src/schema/sqlite.ts", + out: "./apps/backend/drizzle/sqlite", dialect: "sqlite", tablesFilter: ["undb_*"], dbCredentials: { diff --git a/drizzle.turso.config.ts b/drizzle.turso.config.ts index 60b2af033..a6f5d054a 100644 --- a/drizzle.turso.config.ts +++ b/drizzle.turso.config.ts @@ -1,8 +1,8 @@ import { defineConfig } from "drizzle-kit" export default defineConfig({ - schema: "./packages/persistence/src/tables.ts", - out: "./apps/backend/drizzle", + schema: "./packages/persistence/src/schema/sqlite.ts", + out: "./apps/backend/drizzle/sqlite", dialect: "turso", tablesFilter: ["undb_*"], dbCredentials: { diff --git a/package.json b/package.json index c718b8522..d8a0ae76f 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "dev": "bun --bun turbo dev", "lint": "turbo lint", "format": "prettier --write \"**/*.{ts,tsx,md,svelte}\"", - "studio": "drizzle-kit studio --config drizzle.config.ts", + "studio": "drizzle-kit studio --config drizzle.sqlite.config.ts", "studio:turso": "drizzle-kit studio --config drizzle.turso.config.ts", "generate": "run-s generate:db migrate:deploy", - "generate:db": "drizzle-kit generate --config drizzle.config.ts", - "migrate:db": "drizzle-kit push --config drizzle.config.ts", + "generate:db": "run-p generate:db:*", + "generate:db:postgres": "drizzle-kit generate --config drizzle.postgres.config.ts", + "generate:db:sqlite": "drizzle-kit generate --config drizzle.sqlite.config.ts", + "migrate:db": "drizzle-kit push --config drizzle.sqlite.config.ts", "move-assets": "bun run ./scripts/move-assets.ts", "migrate:deploy": "bun run ./scripts/migrate.ts", "prepare": "husky || echo 1", diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index d9dc6d8fa..6d94b5bbc 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -36,6 +36,14 @@ const tursoEnv = createEnv({ emptyStringAsUndefined: true, }) +const postgresEnv = createEnv({ + server: { + UNDB_DB_POSTGRES_URL: z.string().optional(), + }, + runtimeEnv: import.meta.env, + emptyStringAsUndefined: true, +}) + const oauthEnv = createEnv({ server: { UNDB_OAUTH_GITHUB_ENABLED: z @@ -140,7 +148,7 @@ const emailEnv = createEnv({ const dbEnv = createEnv({ server: { - UNDB_DB_PROVIDER: z.enum(["sqlite", "turso"]).default("sqlite").optional(), + UNDB_DB_PROVIDER: z.enum(["sqlite", "turso", "postgres"]).default("sqlite").optional(), UNDB_OUTBOX_SCAN_BATCH_SIZE: z .string() .optional() @@ -163,5 +171,5 @@ export const env = createEnv({ }, runtimeEnv: import.meta.env, emptyStringAsUndefined: true, - extends: [tursoEnv, dbEnv, oauthEnv, storageEnv, s3Env, emailEnv, minioEnv, authEnv], + extends: [tursoEnv, postgresEnv, dbEnv, oauthEnv, storageEnv, s3Env, emailEnv, minioEnv, authEnv], }) diff --git a/packages/persistence/package.json b/packages/persistence/package.json index 6e5d781d1..967abb75a 100644 --- a/packages/persistence/package.json +++ b/packages/persistence/package.json @@ -13,6 +13,7 @@ "type": "module", "devDependencies": { "@types/bun": "latest", + "@types/pg": "^8.11.10", "@types/sql.js": "^1.4.9", "drizzle-kit": "^0.30.1" }, @@ -42,6 +43,8 @@ "kysely": "^0.27.5", "kysely-bun-sqlite": "^0.3.2", "kysely-wasm": "^0.7.0", + "pg": "^8.13.1", + "postgres": "^3.4.5", "radash": "^12.1.0", "reflect-metadata": "^0.2.2", "sql.js": "^1.12.0", diff --git a/packages/persistence/src/client.ts b/packages/persistence/src/client.ts index 6ff08463e..5b0e8434c 100644 --- a/packages/persistence/src/client.ts +++ b/packages/persistence/src/client.ts @@ -14,3 +14,5 @@ export * from "./record" export * from "./space" export * from "./table" export * from "./template" + +export * from "./db.provider" diff --git a/packages/persistence/src/ctx.ts b/packages/persistence/src/ctx.ts index 94bfc94f9..09138f43d 100644 --- a/packages/persistence/src/ctx.ts +++ b/packages/persistence/src/ctx.ts @@ -14,10 +14,10 @@ export const injectContext = () => inject(CTX) @singleton() export class TxContextImpl implements ITxContext { constructor( - @injectQueryBuilder() - private readonly qb: IQueryBuilder, @injectContext() private readonly context: AsyncLocalStorage, + @injectQueryBuilder() + private readonly qb: IQueryBuilder, ) {} withTransaction(callback: () => Promise): Promise { diff --git a/packages/persistence/src/db-client.ts b/packages/persistence/src/db-client.ts index 88f420ad6..0b880cd76 100644 --- a/packages/persistence/src/db-client.ts +++ b/packages/persistence/src/db-client.ts @@ -1,6 +1,7 @@ import { createClient } from "@libsql/client" import { inject } from "@undb/di" import Database from "bun:sqlite" +import pg from "pg" export const DATABASE_CLIENT = Symbol.for("DATABASE_CLIENT") @@ -13,3 +14,9 @@ export const createTursoClient = (url: string, authToken?: string) => { export const createSqliteClient = (fileName: string): Database => { return new Database(fileName) } + +export const createPostgresClient = (connectionString: string) => { + return new pg.Pool({ + connectionString, + }) +} diff --git a/packages/persistence/src/db.provider.ts b/packages/persistence/src/db.provider.ts new file mode 100644 index 000000000..1fc9733b6 --- /dev/null +++ b/packages/persistence/src/db.provider.ts @@ -0,0 +1,5 @@ +import { inject } from "@undb/di" + +export const DB_PROVIDER = Symbol.for("DB_PROVIDER") + +export const injectDbProvider = () => inject(DB_PROVIDER) diff --git a/packages/persistence/src/db.ts b/packages/persistence/src/db.ts index 12cf09dcc..ad3ef71b3 100644 --- a/packages/persistence/src/db.ts +++ b/packages/persistence/src/db.ts @@ -22,7 +22,7 @@ import type { tables, users, webhook, -} from "./tables" +} from "./schema/sqlite" type SpaceTable = Kyselify type TableTable = Kyselify diff --git a/packages/persistence/src/migrate.server.ts b/packages/persistence/src/migrate.server.ts index 06ba368da..e9b4e9b58 100644 --- a/packages/persistence/src/migrate.server.ts +++ b/packages/persistence/src/migrate.server.ts @@ -1,22 +1,35 @@ import type { Client } from "@libsql/client" import { container } from "@undb/di" -import { env } from "@undb/env" import type Database from "bun:sqlite" import { drizzle as sqliteDrizzle } from "drizzle-orm/bun-sqlite" import { migrate as sqliteMigrate } from "drizzle-orm/bun-sqlite/migrator" import { drizzle as libsqlDrizzle } from "drizzle-orm/libsql" import { migrate as libsqlMigrate } from "drizzle-orm/libsql/migrator" +import { drizzle as postgresDrizzle } from "drizzle-orm/node-postgres" +import { migrate as postgresMigrate } from "drizzle-orm/node-postgres/migrator" +import pg from "pg" import { DATABASE_CLIENT } from "./db-client" import { DrizzleLogger } from "./db.logger" +import { DB_PROVIDER } from "./db.provider" export async function dbMigrate() { - if (env.UNDB_DB_PROVIDER === "sqlite" || !env.UNDB_DB_PROVIDER) { + const dbProvider = container.resolve(DB_PROVIDER) + if (dbProvider === "sqlite" || !dbProvider) { const sqlite = container.resolve(DATABASE_CLIENT) const db = sqliteDrizzle(sqlite, { logger: new DrizzleLogger(), }) - sqliteMigrate(db, { migrationsFolder: "./drizzle" }) + sqliteMigrate(db, { migrationsFolder: "./drizzle/sqlite" }) + return + } else if (dbProvider === "postgres") { + const pg = container.resolve(DATABASE_CLIENT) + + const db = postgresDrizzle(pg, { + logger: new DrizzleLogger(), + }) + + await postgresMigrate(db, { migrationsFolder: "./drizzle/postgres" }) return } @@ -25,5 +38,5 @@ export async function dbMigrate() { logger: new DrizzleLogger(), }) - await libsqlMigrate(db, { migrationsFolder: "./drizzle" }) + await libsqlMigrate(db, { migrationsFolder: "./drizzle/sqlite" }) } diff --git a/packages/persistence/src/qb.server.ts b/packages/persistence/src/qb.server.ts index e93cf5bc5..1e361f3ef 100644 --- a/packages/persistence/src/qb.server.ts +++ b/packages/persistence/src/qb.server.ts @@ -1,7 +1,9 @@ import type { Client } from "@libsql/client" import { LibsqlDialect } from "@libsql/kysely-libsql" import { Database as SqliteDatabase } from "bun:sqlite" +import { PostgresDialect } from "kysely" import { BunSqliteDialect } from "kysely-bun-sqlite" +import pg from "pg" import { createQueryBuilderWithDialect } from "./qb.util" export function createTursoQueryBuilder(client: Client) { @@ -19,3 +21,11 @@ export function createSqliteQueryBuilder(sqlite: SqliteDatabase) { }), ) } + +export function createPostgresQueryBuilder(pg: pg.Pool) { + return createQueryBuilderWithDialect( + new PostgresDialect({ + pool: pg, + }), + ) +} diff --git a/packages/persistence/src/record/record-query-creator-visitor.ts b/packages/persistence/src/record/record-query-creator-visitor.ts index fa88f6452..80dd8531f 100644 --- a/packages/persistence/src/record/record-query-creator-visitor.ts +++ b/packages/persistence/src/record/record-query-creator-visitor.ts @@ -33,10 +33,11 @@ import type { FormulaField } from "@undb/table/src/modules/schema/fields/variant import { getTableName } from "drizzle-orm" import { sql, type QueryCreator, type SelectExpression } from "kysely" import type { IRecordQueryBuilder } from "../qb.type" -import { users } from "../tables" +import { users } from "../schema/sqlite" import { JoinTable } from "../underlying/reference/join-table" import { UnderlyingTable } from "../underlying/underlying-table" import { getRollupFn } from "../underlying/underlying-table.util" +import type { IDatabaseFnUtil } from "../utils/fn.util" import { getJsonExpandedFieldName } from "./record-utils" export class RecordQueryCreatorVisitor implements IFieldVisitor { @@ -45,6 +46,7 @@ export class RecordQueryCreatorVisitor implements IFieldVisitor { private readonly table: TableDo, private readonly foreignTables: Map, private readonly visibleFields: Field[], + private readonly dbFnUtil: IDatabaseFnUtil, ) {} #creator: QueryCreator | null = null @@ -147,14 +149,18 @@ export class RecordQueryCreatorVisitor implements IFieldVisitor { (sb) => [ `${name}.${valueField} as ${ID_TYPE}`, - visible ? sb.fn("json_group_array", [sb.ref(`${name}.${symmetricField}`)]).as(field.id.value) : undefined, + visible + ? sb.fn(this.dbFnUtil.jsonGroupArray, [sb.ref(`${name}.${symmetricField}`)]).as(field.id.value) + : undefined, // select display fields for reference ...displayFields.map((f) => - sb.fn("json_group_array", [sb.ref(`${underlyingForiegnTable.name}.${f.id.value}`)]).as(f.id.value), + sb + .fn(this.dbFnUtil.jsonGroupArray, [sb.ref(`${underlyingForiegnTable.name}.${f.id.value}`)]) + .as(f.id.value), ), ...rollupFields.map((rollupField) => sb - .fn(getRollupFn(rollupField.fn), [ + .fn(getRollupFn(rollupField.fn, this.dbFnUtil), [ sb.ref(`${underlyingForiegnTable.name}.${rollupField.rollupFieldId}`), ]) .as(rollupField.id.value), diff --git a/packages/persistence/src/record/record-query.helper.ts b/packages/persistence/src/record/record-query.helper.ts index 3fddf97bd..eafd0faa8 100644 --- a/packages/persistence/src/record/record-query.helper.ts +++ b/packages/persistence/src/record/record-query.helper.ts @@ -1,13 +1,15 @@ import { injectContext, type IContext } from "@undb/context" -import { singleton } from "@undb/di" +import { inject, singleton } from "@undb/di" import type { IPagination, Option } from "@undb/domain" import { FieldIdVo, type Field, type IViewSort, type RecordComositeSpecification, type TableDo } from "@undb/table" import { sql, type ExpressionBuilder, type SelectQueryBuilder } from "kysely" import type { ITxContext } from "../ctx.interface" import { injectTxCTX } from "../ctx.provider" +import { injectDbProvider } from "../db.provider" import { injectQueryBuilder } from "../qb.provider" import type { IRecordQueryBuilder } from "../qb.type" import { UnderlyingTable } from "../underlying/underlying-table" +import { DatabaseFnUtil, type IDatabaseFnUtil } from "../utils/fn.util" import { RecordQueryCreatorVisitor } from "./record-query-creator-visitor" import { RecordQuerySpecCreatorVisitor } from "./record-query-spec-creator-visitor" import { RecordReferenceVisitor } from "./record-reference-visitor" @@ -24,6 +26,10 @@ export class RecordQueryHelper { private readonly context: IContext, @injectTxCTX() private readonly txContext: ITxContext, + @injectDbProvider() + private readonly dbProvider: string, + @inject(DatabaseFnUtil) + private readonly dbFnUtil: IDatabaseFnUtil, ) {} createQueryCreator( @@ -34,7 +40,7 @@ export class RecordQueryHelper { ) { const trx = this.txContext.getAnonymousTransaction() - let qb = new RecordQueryCreatorVisitor(trx, table, foreignTables, visibleFields).create() + let qb = new RecordQueryCreatorVisitor(trx, table, foreignTables, visibleFields, this.dbFnUtil).create() const visitor = new RecordQuerySpecCreatorVisitor(trx, qb, table) if (spec.isSome()) { spec.unwrap().accept(visitor) @@ -49,6 +55,7 @@ export class RecordQueryHelper { foreignTables: Map, visibleFields: Field[], spec: Option, + aggregate = false, ) { const t = new UnderlyingTable(table) const qb = this.createQueryCreator(table, foreignTables, visibleFields, spec) @@ -57,7 +64,11 @@ export class RecordQueryHelper { .selectFrom(table.id.value) .$call((qb) => new RecordReferenceVisitor(qb, table).join(visibleFields)) .$if(spec.isSome(), (qb) => new RecordSpecReferenceVisitor(qb, table).$join(spec.unwrap())) - .select((sb) => new RecordSelectFieldVisitor(t, foreignTables, sb).$select(visibleFields)) + .$if(!aggregate, (qb) => + qb.select((sb) => + new RecordSelectFieldVisitor(t, foreignTables, sb, this.dbProvider, this.dbFnUtil).$select(visibleFields), + ), + ) } handleWhere(table: TableDo, spec: Option) { diff --git a/packages/persistence/src/record/record-select-field-visitor.ts b/packages/persistence/src/record/record-select-field-visitor.ts index 44b4a2dcb..25f6d086c 100644 --- a/packages/persistence/src/record/record-select-field-visitor.ts +++ b/packages/persistence/src/record/record-select-field-visitor.ts @@ -32,9 +32,10 @@ import { import type { FormulaField } from "@undb/table/src/modules/schema/fields/variants/formula-field" import { getTableName } from "drizzle-orm" import { sql, type ExpressionBuilder, type SelectExpression } from "kysely" -import { users } from "../tables" +import { users } from "../schema/sqlite" import type { UnderlyingTable } from "../underlying/underlying-table" import { getDateRangeFieldName } from "../underlying/underlying-table.util" +import type { IDatabaseFnUtil } from "../utils/fn.util" import { createDisplayFieldName } from "./record-utils" export class RecordSelectFieldVisitor implements IFieldVisitor { @@ -59,11 +60,15 @@ export class RecordSelectFieldVisitor implements IFieldVisitor { private readonly table: UnderlyingTable, private readonly foreignTables: Map, private readonly eb: ExpressionBuilder, + private readonly dbProvider: string, + private readonly dbFnUtil: IDatabaseFnUtil, ) { this.#addSelect(this.getField(ID_TYPE)) } #selectSingelUser(field: UserField | CreatedByField | UpdatedByField) { + const db = this.dbProvider + const as = createDisplayFieldName(field) const user = getTableName(users) @@ -71,7 +76,7 @@ export class RecordSelectFieldVisitor implements IFieldVisitor { .selectFrom(user) .select( this.eb - .fn("json_object", [ + .fn(this.dbFnUtil.jsonObject, [ sql.raw("'username'"), this.eb.fn.coalesce(`${user}.${users.username.name}`, sql`NULL`), sql.raw("'email'"), @@ -122,8 +127,7 @@ export class RecordSelectFieldVisitor implements IFieldVisitor { } button(field: ButtonField): void {} currency(field: CurrencyField): void { - const fieldName = this.getField(field.id.value) - const selection = sql`${sql.raw(fieldName)} / 100.0`.as(field.id.value) + const selection = sql`${sql.raw(`${this.table.name}."${field.id.value}"`)} / 100.0`.as(field.id.value) this.#addSelect(selection) } rating(field: RatingField): void { @@ -155,7 +159,7 @@ export class RecordSelectFieldVisitor implements IFieldVisitor { const displayFields = foreignTable.schema.getDisplayFields() const select = this.eb .fn( - "json_object", + this.dbFnUtil.jsonObject, displayFields.flatMap((displayField) => [ sql.raw(`'${displayField.id.value}'`), `${field.id.value}.${displayField.id.value}`, @@ -181,7 +185,7 @@ export class RecordSelectFieldVisitor implements IFieldVisitor { } dateRange(field: DateRangeField): void { const { start, end } = getDateRangeFieldName(field) - this.#addSelect(this.eb.fn("json_array", [this.getField(start), this.getField(end)]).as(field.id.value)) + this.#addSelect(this.eb.fn(this.dbFnUtil.jsonArray, [this.getField(start), this.getField(end)]).as(field.id.value)) } checkbox(field: CheckboxField): void { this.#addSelect(this.getField(field.id.value)) @@ -199,8 +203,8 @@ export class RecordSelectFieldVisitor implements IFieldVisitor { .when(`${this.table.name}.${field.id.value}`, "is", null) .then(null) .else( - this.eb.fn("json_group_array", [ - this.eb.fn("json_object", [ + this.eb.fn(this.dbFnUtil.jsonGroupArray, [ + this.eb.fn(this.dbFnUtil.jsonObject, [ sql.raw("'username'"), this.eb.fn.coalesce(`${field.id.value}.${users.username.name}`, sql`NULL`), sql.raw("'email'"), diff --git a/packages/persistence/src/record/record-utils.ts b/packages/persistence/src/record/record-utils.ts index 1c7bc85a1..2bb233dcd 100644 --- a/packages/persistence/src/record/record-utils.ts +++ b/packages/persistence/src/record/record-utils.ts @@ -176,6 +176,11 @@ export function getRecordDTOFromEntity(table: TableDo, entity: any, foreignTable } } + if (field.type === "currency") { + values[key] = Number(value) + continue + } + if (field.type === "checkbox") { values[key] = Boolean(value) continue @@ -191,6 +196,11 @@ export function getRecordDTOFromEntity(table: TableDo, entity: any, foreignTable values[key] = [start?.toISOString() ?? null, end?.toISOString() ?? null] continue } + if (field.type === "autoIncrement") { + values[key] = Number(value) + continue + } + values[key] = value } } diff --git a/packages/persistence/src/record/record.mutate-visitor.ts b/packages/persistence/src/record/record.mutate-visitor.ts index d413639ce..832f32654 100644 --- a/packages/persistence/src/record/record.mutate-visitor.ts +++ b/packages/persistence/src/record/record.mutate-visitor.ts @@ -90,19 +90,24 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR private readonly qb: IRecordQueryBuilder, private readonly eb: ExpressionBuilder, private readonly context: IContext, + private readonly dbProvider: string, ) { super() } + #setDate(fieldId: string, value: Date | null) { + if (value) { + this.setData(fieldId, this.dbProvider === "postgres" ? value : value.getTime()) + } else { + this.setData(fieldId, null) + } + } + idIn(spec: IdIn): void { throw new Error("Method not implemented.") } checkboxEqual(spec: CheckboxEqual): void { - if (!spec.value) { - this.setData(spec.fieldId.value, false) - } else { - this.setData(spec.fieldId.value, spec.value) - } + this.setData(spec.fieldId.value, Boolean(spec.value)) } jsonEqual(spec: JsonEqual): void { if (!spec.json) { @@ -125,33 +130,33 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR dateEqual(spec: DateEqual): void { if (spec.date === "@now") { const start = startOfDay(new Date()) - this.setData(spec.fieldId.value, start.getTime()) + this.#setDate(spec.fieldId.value, start) } else if (spec.date === "@today") { const start = startOfToday() - this.setData(spec.fieldId.value, start.getTime()) + this.#setDate(spec.fieldId.value, start) } else if (spec.date === "@yesterday") { const start = startOfYesterday() - this.setData(spec.fieldId.value, start.getTime()) + this.#setDate(spec.fieldId.value, start) } else if (spec.date === "@tomorrow") { const start = startOfTomorrow() - this.setData(spec.fieldId.value, start.getTime()) + this.#setDate(spec.fieldId.value, start) } else { - this.setData(spec.fieldId.value, spec.date?.getTime() ?? null) + this.#setDate(spec.fieldId.value, spec.date) } } dateRangeEqual(spec: DateRangeEqual): void { const field = this.table.schema.getFieldById(new FieldIdVo(spec.fieldId.value)).expect("No field found") const { start, end } = getDateRangeFieldName(field as DateRangeField) - this.setData(start, spec.dateRange.start?.getTime() ?? null) - this.setData(end, spec.dateRange.end?.getTime() ?? null) + this.#setDate(start, spec.dateRange.start) + this.#setDate(end, spec.dateRange.end) } dateRangeIsEmpty(spec: DateRangeIsEmpty): void { const field = this.table.schema.getFieldById(new FieldIdVo(spec.fieldId.value)).expect("No field found") const { start, end } = getDateRangeFieldName(field as DateRangeField) - this.setData(start, null) - this.setData(end, null) + this.#setDate(start, null) + this.#setDate(end, null) } dateRangeDateIsAfter(spec: DateRangeDateIsAfter): void { throw new Error("Method not implemented.") @@ -444,7 +449,7 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR this.setData(s.fieldId.value, s.value || null) } clone(): this { - return new RecordMutateVisitor(this.table, this.record, this.qb, this.eb, this.context) as this + return new RecordMutateVisitor(this.table, this.record, this.qb, this.eb, this.context, this.dbProvider) as this } formulaEqual(s: FormulaEqual): void { throw new Error("Method not implemented.") diff --git a/packages/persistence/src/record/record.query-repository.ts b/packages/persistence/src/record/record.query-repository.ts index c5767f9ee..ba17c6082 100644 --- a/packages/persistence/src/record/record.query-repository.ts +++ b/packages/persistence/src/record/record.query-repository.ts @@ -29,28 +29,28 @@ import { getTableName } from "drizzle-orm" import { sql, type AliasedExpression, type Expression, type ExpressionBuilder } from "kysely" import { injectQueryBuilder } from "../qb.provider" import type { IRecordQueryBuilder } from "../qb.type" -import { users } from "../tables" +import { users } from "../schema/sqlite" import { UnderlyingTable } from "../underlying/underlying-table" +import { DatabaseFnUtil, type IDatabaseFnUtil } from "../utils/fn.util" import { RecordQueryHelper } from "./record-query.helper" import { RecordReferenceVisitor } from "./record-reference-visitor" import { RecordSpecReferenceVisitor } from "./record-spec-reference-visitor" import { getRecordDTOFromEntity } from "./record-utils" import { AggregateFnBuiler } from "./record.aggregate-builder" -import { RecordMapper } from "./record.mapper" @singleton() export class RecordQueryRepository implements IRecordQueryRepository { constructor( @injectQueryBuilder() private readonly qb: IRecordQueryBuilder, - @inject(RecordMapper) - private readonly mapper: RecordMapper, @injectTableRepository() private readonly tableRepo: ITableRepository, @inject(RecordQueryHelper) private readonly helper: RecordQueryHelper, @injectContext() private readonly context: IContext, + @inject(DatabaseFnUtil) + private readonly dbFnUtil: IDatabaseFnUtil, ) {} async count(tableId: TableId): Promise { @@ -67,7 +67,7 @@ export class RecordQueryRepository implements IRecordQueryRepository { const selectFields = table.getSelectFields(view, undefined) const foreignTables = await this.getForeignTables(table, selectFields) - const qb = this.helper.createQuery(table, foreignTables, selectFields, spec) + const qb = this.helper.createQuery(table, foreignTables, selectFields, spec, true) const { total } = await qb .select((eb) => eb.fn.countAll().as("total")) @@ -181,7 +181,7 @@ export class RecordQueryRepository implements IRecordQueryRepository { .selectFrom(user) .select( eb - .fn("json_object", [ + .fn(this.dbFnUtil.jsonObject, [ sql.raw("'username'"), eb.fn.coalesce(`${user}.${users.username.name}`, sql`NULL`), sql.raw("'email'"), @@ -209,9 +209,9 @@ export class RecordQueryRepository implements IRecordQueryRepository { throw new Error("value field is required") } - let valueFieldAlias = `${t.name}.${valueField.id.value}` + let valueFieldAlias = `${t.name}."${valueField.id.value}"` if (valueField.type === "currency") { - valueFieldAlias = `${t.name}.${valueField.id.value} / 100` + valueFieldAlias = `${t.name}."${valueField.id.value}" / 100` } const caseString = @@ -224,7 +224,7 @@ export class RecordQueryRepository implements IRecordQueryRepository { }) .flat() - selects.push(eb.fn("json_object", columnSelects).as("values")) + selects.push(eb.fn(this.dbFnUtil.jsonObject, columnSelects).as("values")) if (aggFn === "count") { selects.push(sql.raw(`sum(CASE WHEN "${t.name}"."${columnField.id.value}" IS NOT NULL THEN 1 END)`).as(`agg`)) diff --git a/packages/persistence/src/record/record.repository.ts b/packages/persistence/src/record/record.repository.ts index c87efe034..0757e1214 100644 --- a/packages/persistence/src/record/record.repository.ts +++ b/packages/persistence/src/record/record.repository.ts @@ -22,6 +22,7 @@ import { chunk } from "es-toolkit/array" import { sql, type CompiledQuery, type ExpressionBuilder } from "kysely" import type { ITxContext } from "../ctx.interface" import { injectTxCTX } from "../ctx.provider" +import { injectDbProvider } from "../db.provider" import { UnderlyingTable } from "../underlying/underlying-table" import { RecordQueryHelper } from "./record-query.helper" import { getRecordDTOFromEntity } from "./record-utils" @@ -43,6 +44,8 @@ export class RecordRepository implements IRecordRepository { private readonly context: IContext, @injectTxCTX() private readonly txContext: ITxContext, + @injectDbProvider() + private readonly dbProvider: string, ) {} private async getForeignTables(table: TableDo, fields: Field[]): Promise> { @@ -70,7 +73,7 @@ export class RecordRepository implements IRecordRepository { await trx .insertInto(t.name) .values((eb) => { - const visitor = new RecordMutateVisitor(table, record, trx, eb, this.context) + const visitor = new RecordMutateVisitor(table, record, trx, eb, this.context, this.dbProvider) spec.accept(visitor) sql.push(...visitor.sql) @@ -101,7 +104,7 @@ export class RecordRepository implements IRecordRepository { .values((eb) => records.map((record) => { const spec = record.toInsertSpec(table) - const visitor = new RecordMutateVisitor(table, record, trx, eb, this.context) + const visitor = new RecordMutateVisitor(table, record, trx, eb, this.context, this.dbProvider) spec.accept(visitor) sql.push(...visitor.sql) @@ -212,7 +215,7 @@ export class RecordRepository implements IRecordRepository { await trx .updateTable(t.name) .set((eb) => { - const visitor = new RecordMutateVisitor(table, record, trx, eb, this.context) + const visitor = new RecordMutateVisitor(table, record, trx, eb, this.context, this.dbProvider) spec.unwrap().accept(visitor) sql.push(...visitor.sql) return { ...visitor.data, [UPDATED_BY_TYPE]: userId } @@ -247,13 +250,13 @@ export class RecordRepository implements IRecordRepository { let data = {} if (records.length) { for (const record of records) { - const visitor = new RecordMutateVisitor(table, record, trx, eb, this.context) + const visitor = new RecordMutateVisitor(table, record, trx, eb, this.context, this.dbProvider) update.accept(visitor) queries.push(...visitor.sql) data = { ...data, ...visitor.data } } } else { - const visitor = new RecordMutateVisitor(table, null, trx, eb, this.context) + const visitor = new RecordMutateVisitor(table, null, trx, eb, this.context, this.dbProvider) update.accept(visitor) queries.push(...visitor.sql) data = visitor.data diff --git a/packages/persistence/src/schema/postgres.ts b/packages/persistence/src/schema/postgres.ts new file mode 100644 index 000000000..c899659c5 --- /dev/null +++ b/packages/persistence/src/schema/postgres.ts @@ -0,0 +1,424 @@ +import type { IAuditDetail } from "@undb/audit" +import type { IInvitationStatus, ISpaceMemberRole, ISpaceMemberWithoutOwner } from "@undb/authz" +import type { IDashboardLayouts, IDashboardWidgets } from "@undb/dashboard" +import type { RECORD_EVENTS } from "@undb/table" +import type { IWebhookMethod } from "@undb/webhook" +import { sql } from "drizzle-orm" +import { + bigint, + boolean, + index, + integer, + jsonb, + pgTableCreator, + primaryKey, + text, + timestamp, + unique, +} from "drizzle-orm/pg-core" + +const pgTable = pgTableCreator((name) => `undb_${name}`) + +export const space = pgTable( + "space", + { + id: text("id").notNull().primaryKey(), + name: text("name"), + isPersonal: boolean("is_personal").notNull(), + avatar: text("avatar"), + createdAt: text("created_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + createdBy: text("created_by") + .notNull() + .references(() => users.id), + updateAt: text("updated_at") + .notNull() + .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), + updatedBy: text("updated_by") + .notNull() + .references(() => users.id), + deletedAt: bigint("deleted_at", { mode: "bigint" }), + deletedBy: text("deleted_by").references(() => users.id), + }, + (table) => [index("space_name_idx").on(table.name)], +) + +export const tables = pgTable( + "table", + { + id: text("id").notNull().primaryKey(), + name: text("name").notNull(), + baseId: text("base_id") + .notNull() + .references(() => baseTable.id), + + spaceId: text("space_id") + .notNull() + .references(() => space.id), + + schema: jsonb("schema").notNull(), + views: jsonb("views").notNull(), + forms: jsonb("forms"), + rls: jsonb("rls"), + widgets: jsonb("widgets"), + + createdAt: text("created_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + createdBy: text("created_by") + .notNull() + .references(() => users.id), + updateAt: text("updated_at") + .notNull() + .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), + updatedBy: text("updated_by") + .notNull() + .references(() => users.id), + }, + (table) => [ + index("table_base_id_idx").on(table.baseId), + unique("table_name_unique_idx").on(table.name, table.baseId), + index("table_space_id_idx").on(table.spaceId), + ], +) + +export const tableIdMapping = pgTable( + "table_id_mapping", + { + tableId: text("table_id") + .notNull() + .references(() => tables.id), + subjectId: text("subject_id").notNull(), + }, + (table) => [primaryKey({ columns: [table.tableId, table.subjectId] })], +) + +export const referenceIdMapping = pgTable( + "reference_id_mapping", + { + fieldId: text("field_id").notNull(), + tableId: text("table_id").notNull(), + symmetricFieldId: text("symmetric_field_id"), + foreignTableId: text("foreign_table_id").notNull(), + }, + (table) => [ + unique("reference_id_mapping_unique_idx").on( + table.fieldId, + table.tableId, + table.symmetricFieldId, + table.foreignTableId, + ), + ], +) + +export const rollupIdMapping = pgTable( + "rollup_id_mapping", + { + fieldId: text("field_id").notNull(), + tableId: text("table_id").notNull(), + rollupId: text("rollup_id").notNull(), + rollupTableId: text("rollup_table_id").notNull(), + }, + (table) => [primaryKey({ columns: [table.fieldId, table.rollupId] })], +) + +export const dashboards = pgTable( + "dashboard", + { + id: text("id").notNull().primaryKey(), + name: text("name").notNull(), + description: text("description"), + baseId: text("base_id") + .notNull() + .references(() => baseTable.id), + spaceId: text("space_id") + .notNull() + .references(() => space.id), + + widgets: jsonb("widgets").$type(), + layout: jsonb("layout").$type(), + + createdAt: text("created_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + createdBy: text("created_by") + .notNull() + .references(() => users.id), + updateAt: text("updated_at") + .notNull() + .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), + updatedBy: text("updated_by") + .notNull() + .references(() => users.id), + }, + (table) => [ + index("dashboard_base_id_idx").on(table.baseId), + unique("dashboard_name_unique_idx").on(table.name, table.baseId), + index("dashboard_space_id_idx").on(table.spaceId), + ], +) + +export const dashboardTableIdMapping = pgTable( + "dashboard_table_id_mapping", + { + dashboardId: text("dashboard_id") + .notNull() + .references(() => dashboards.id), + tableId: text("table_id") + .notNull() + .references(() => tables.id), + }, + (table) => [primaryKey({ columns: [table.dashboardId, table.tableId] })], +) + +export const attachments = pgTable( + "attachment", + { + id: text("id").notNull().primaryKey(), + name: text("name").notNull(), + size: integer("size").notNull(), + mimeType: text("mime_type").notNull(), + url: text("url").notNull(), + token: text("token"), + createdAt: bigint("created_at", { mode: "bigint" }).notNull(), + createdBy: text("created_by") + .notNull() + .references(() => users.id), + spaceId: text("space_id") + .notNull() + .references(() => space.id), + }, + (table) => [index("attachment_size_idx").on(table.size), index("attachment_space_id_idx").on(table.spaceId)], +) + +export const attachmentMapping = pgTable( + "attachment_mapping", + { + attachmentId: text("attachment_id") + .notNull() + .references(() => attachments.id), + tableId: text("table_id") + .notNull() + .references(() => tables.id), + recordId: text("record_id").notNull(), + fieldId: text("field_id").notNull(), + }, + (table) => [primaryKey({ columns: [table.attachmentId, table.tableId, table.recordId, table.fieldId] })], +) + +export const outbox = pgTable( + "outbox", + { + id: text("id").notNull().primaryKey(), + payload: jsonb("payload").notNull(), + meta: jsonb("meta"), + timestamp: bigint("timestamp", { mode: "bigint" }).notNull(), + userId: text("user_id"), + name: text("name").notNull(), + spaceId: text("space_id") + .notNull() + .references(() => space.id), + }, + (table) => [index("outbox_space_id_idx").on(table.spaceId)], +) + +export const users = pgTable( + "user", + { + id: text("id").notNull().primaryKey(), + username: text("username").notNull(), + email: text("email").notNull().unique(), + email_verified: boolean("email_verified").notNull().default(false), + password: text("password").notNull(), + avatar: text("avatar"), + otp_secret: text("otp_secret"), + }, + (table) => [index("user_username_idx").on(table.username), index("user_email_idx").on(table.email)], +) + +export const passwordResetTokenTable = pgTable( + "password_reset_token", + { + id: integer().primaryKey().generatedByDefaultAsIdentity(), + token: text("token").notNull().unique(), + userId: text("user_id") + .notNull() + .references(() => users.id), + expiresAt: bigint("expires_at", { mode: "bigint" }).notNull(), + }, + (table) => [index("password_reset_token_user_id_idx").on(table.userId)], +) + +export const oauthAccount = pgTable( + "oauth_account", + { + provider_id: text("provider_id").notNull(), + provider_user_id: text("provider_user_id").notNull(), + user_id: text("user_id") + .notNull() + .references(() => users.id), + }, + (table) => [primaryKey({ columns: [table.provider_id, table.provider_user_id] })], +) + +export const emailVerificationCode = pgTable("email_verification_code", { + id: integer().primaryKey().generatedByDefaultAsIdentity(), + code: text("code").notNull(), + userId: text("user_id") + .unique() + .references(() => users.id), + email: text("email").notNull(), + expires_at: bigint("expires_at", { mode: "bigint" }).notNull(), +}) + +export const sessionTable = pgTable("session", { + id: text("id").notNull().primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + expiresAt: timestamp("expires_at", { mode: "date", withTimezone: true }).notNull(), + spaceId: text("space_id") + .notNull() + .references(() => space.id), +}) + +export const webhook = pgTable( + "webhook", + { + id: text("id").notNull().primaryKey(), + name: text("name").notNull(), + url: text("url").notNull(), + method: text("method").notNull().$type(), + enabled: boolean("enabled").notNull(), + tableId: text("table_id") + .notNull() + .references(() => tables.id), + headers: jsonb("headers").notNull(), + condition: jsonb("condition"), + event: text("event").notNull().$type(), + spaceId: text("space_id") + .notNull() + .references(() => space.id), + }, + (table) => [ + index("webhook_table_id_idx").on(table.tableId), + index("webhook_space_id_idx").on(table.spaceId), + index("webhook_url_idx").on(table.url), + ], +) + +export const audit = pgTable( + "audit", + { + id: text("id").notNull().primaryKey(), + timestamp: bigint("timestamp", { mode: "bigint" }).notNull(), + detail: jsonb("detail").$type(), + meta: jsonb("meta"), + op: text("op").notNull().$type(), + tableId: text("table_id").notNull(), + recordId: text("record_id").notNull(), + operatorId: text("operator_id").notNull(), + spaceId: text("space_id") + .notNull() + .references(() => space.id), + }, + (table) => [ + index("audit_table_id_idx").on(table.tableId), + index("audit_space_id_idx").on(table.spaceId), + index("audit_record_id_idx").on(table.recordId), + ], +) + +export const spaceMember = pgTable( + "space_member", + { + id: text("id").notNull().primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + role: text("role").notNull().$type(), + spaceId: text("space_id") + .notNull() + .references(() => space.id), + }, + (table) => [unique("space_member_unique_idx").on(table.userId, table.spaceId)], +) + +export const baseTable = pgTable( + "base", + { + id: text("id").notNull().primaryKey(), + name: text("name").notNull(), + spaceId: text("space_id") + .references(() => space.id) + .notNull(), + createdAt: text("created_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + createdBy: text("created_by") + .notNull() + .references(() => users.id), + updateAt: text("updated_at") + .notNull() + .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), + updatedBy: text("updated_by") + .notNull() + .references(() => users.id), + }, + (base) => [unique("base_name_unique_idx").on(base.name, base.spaceId), index("base_space_id_idx").on(base.spaceId)], +) + +export const shareTable = pgTable( + "share", + { + id: text("id").notNull().primaryKey(), + targetType: text("target_type").notNull(), + targetId: text("target_id").notNull(), + enabled: boolean("enabled").notNull(), + spaceId: text("space_id") + .notNull() + .references(() => space.id), + }, + (table) => [ + unique("share_unique_idx").on(table.targetType, table.targetId), + index("share_space_id_idx").on(table.spaceId), + ], +) + +export const invitations = pgTable( + "invitation", + { + id: text("id").notNull().primaryKey(), + email: text("email").notNull(), + role: text("role").notNull().$type(), + status: text("status").notNull().$type(), + spaceId: text("space_id") + .references(() => space.id) + .notNull(), + invitedAt: bigint("invited_at", { mode: "bigint" }).notNull(), + inviterId: text("inviter_id") + .notNull() + .references(() => users.id), + }, + (table) => [ + index("invitation_space_id_idx").on(table.spaceId), + unique("invitation_unique_idx").on(table.email, table.spaceId), + ], +) + +export const apiTokenTable = pgTable( + "api_token", + { + id: text("id").notNull().primaryKey(), + name: text("name").notNull(), + userId: text("user_id") + .notNull() + .references(() => users.id), + spaceId: text("space_id") + .references(() => space.id) + .notNull(), + token: text("token").notNull().unique(), + }, + (table) => [index("api_token_space_id_idx").on(table.spaceId), index("api_token_user_id_idx").on(table.userId)], +) diff --git a/packages/persistence/src/tables.ts b/packages/persistence/src/schema/sqlite.ts similarity index 100% rename from packages/persistence/src/tables.ts rename to packages/persistence/src/schema/sqlite.ts diff --git a/packages/persistence/src/server.ts b/packages/persistence/src/server.ts index faaf8eae8..3eb7889e1 100644 --- a/packages/persistence/src/server.ts +++ b/packages/persistence/src/server.ts @@ -9,10 +9,10 @@ export * from "./migrate.server" export * from "./qb.provider" export * from "./qb.server" export * from "./record" +export * from "./schema/sqlite" export * from "./share" export * from "./space" export * from "./table" -export * from "./tables" export * from "./template" export * from "./user" export * from "./webhook" @@ -20,6 +20,14 @@ export * from "./webhook" export { type Client } from "@libsql/client" export * from "./ctx.interface" export * from "./ctx.provider" -export { DATABASE_CLIENT, createSqliteClient, createTursoClient, injectDatabaseClient } from "./db-client" +export { + DATABASE_CLIENT, + createPostgresClient, + createSqliteClient, + createTursoClient, + injectDatabaseClient, +} from "./db-client" +export * from "./db.provider" export { injectQueryBuilder } from "./qb.provider" export { type IQueryBuilder } from "./qb.type" +export { sessionTable as pgSessionTable, users as pgUsers } from "./schema/postgres" diff --git a/packages/persistence/src/table/table.mutation-visitor.ts b/packages/persistence/src/table/table.mutation-visitor.ts index ba41ff615..401d3a071 100644 --- a/packages/persistence/src/table/table.mutation-visitor.ts +++ b/packages/persistence/src/table/table.mutation-visitor.ts @@ -38,7 +38,7 @@ import type { import { AbstractQBMutationVisitor } from "../abstract-qb.visitor" import { type IQueryBuilder } from "../qb.type" import { json } from "../qb.util" -import { tables } from "../tables" +import { tables } from "../schema/sqlite" export class TableMutationVisitor extends AbstractQBMutationVisitor implements ITableSpecVisitor { constructor( diff --git a/packages/persistence/src/template/template-data.ts b/packages/persistence/src/template/template-data.ts index 4a3507a5e..c67d4e6cd 100644 --- a/packages/persistence/src/template/template-data.ts +++ b/packages/persistence/src/template/template-data.ts @@ -1,5 +1,5 @@ import { env } from "@undb/env" -import { templates,type IBaseTemplateDTO,type ITemplateDTO } from "@undb/template" +import { templates, type IBaseTemplateDTO, type ITemplateDTO } from "@undb/template" function getTemplateImage(folder: string, file: string) { return env.UNDB_BASE_URL + "/assets/templates/" + folder + "/" + file @@ -440,16 +440,14 @@ export const templateData: ITemplateDTO[] = [ }, ] -// if (env.NODE_ENV === "development") { -// templateData.unshift({ -// id: "everything", -// icon: "💼", -// name: "Everything", -// categories: ["sales"], -// description: "A template for testing", -// template: { -// type: "base", -// template: templates.everything as IBaseTemplateDTO, -// }, -// }) -// } +templateData.unshift({ + id: "everything", + icon: "💼", + name: "Everything", + categories: ["sales"], + description: "A template for testing", + template: { + type: "base", + template: templates.everything as IBaseTemplateDTO, + }, +}) diff --git a/packages/persistence/src/underlying/conversion/strategies/string-to-user.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/string-to-user.strategy.ts index 342055efe..8abc6b12a 100644 --- a/packages/persistence/src/underlying/conversion/strategies/string-to-user.strategy.ts +++ b/packages/persistence/src/underlying/conversion/strategies/string-to-user.strategy.ts @@ -1,7 +1,7 @@ import type { Field } from "@undb/table" import { getTableName } from "drizzle-orm" import { sql } from "kysely" -import { users } from "../../../tables" +import { users } from "../../../schema/sqlite" import { UnderlyingConversionStrategy } from "../conversion.interface" export class StringToUserStrategy extends UnderlyingConversionStrategy { diff --git a/packages/persistence/src/underlying/conversion/strategies/user-to-string.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/user-to-string.strategy.ts index 25e2400a4..d08f40d4b 100644 --- a/packages/persistence/src/underlying/conversion/strategies/user-to-string.strategy.ts +++ b/packages/persistence/src/underlying/conversion/strategies/user-to-string.strategy.ts @@ -1,7 +1,7 @@ import { type Field } from "@undb/table" import { getTableName } from "drizzle-orm" import { CaseWhenBuilder, sql } from "kysely" -import { users } from "../../../tables" +import { users } from "../../../schema/sqlite" import { UnderlyingConversionStrategy } from "../conversion.interface" export class UserToStringStrategy extends UnderlyingConversionStrategy { diff --git a/packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts b/packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts index d97dc6ded..68f396094 100644 --- a/packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts @@ -33,6 +33,7 @@ import type { FormulaField } from "@undb/table/src/modules/schema/fields/variant import { AlterTableBuilder, sql } from "kysely" import { AbstractQBMutationVisitor } from "../abstract-qb.visitor" import type { IRecordQueryBuilder } from "../qb.type" +import type { IDatabaseFnUtil } from "../utils/fn.util" import { getUnderlyingFormulaType } from "./underlying-formula.util" import { UnderlyingFormulaVisitor } from "./underlying-formula.visitor" import type { UnderlyingTable } from "./underlying-table" @@ -43,6 +44,7 @@ export class UnderlyingTableFieldUpdatedVisitor extends AbstractQBMutationVisito private readonly table: UnderlyingTable, private readonly prev: Field, private readonly tb: AlterTableBuilder, + private readonly dbFnUtil: IDatabaseFnUtil, ) { super() } @@ -85,16 +87,17 @@ export class UnderlyingTableFieldUpdatedVisitor extends AbstractQBMutationVisito if (deletedOptions.length === 0) { return } + const jsonGroupArray = this.dbFnUtil.jsonGroupArray const query = this.qb .updateTable(tableId) .set({ [field.id.value]: sql`( - SELECT json_group_array(value) - FROM json_each(${sql.raw(tableId + "." + field.id.value)}) + SELECT ${sql.raw(jsonGroupArray)}(value) + FROM json_each(${sql.raw(`${tableId}."${field.id.value}"`)}) WHERE value NOT IN (${sql.join(deletedOptions)}) )`, }) - .where(sql`json_array_length(${sql.raw(tableId + "." + field.id.value)})`, ">", 0) + .where(sql`json_array_length(${sql.raw(`${tableId}."${field.id.value}"`)})`, ">", 0) .compile() this.addSql(query) diff --git a/packages/persistence/src/underlying/underlying-table-field.visitor.ts b/packages/persistence/src/underlying/underlying-table-field.visitor.ts index 4b2e1ff2c..960f83585 100644 --- a/packages/persistence/src/underlying/underlying-table-field.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-field.visitor.ts @@ -45,6 +45,7 @@ export class UnderlyingTableFieldVisitor private readonly qb: IQueryBuilder, private readonly t: UnderlyingTable, public tb: TB, + private readonly dbProvider: string, public readonly isNew: boolean = false, ) {} public atb: AlterTableColumnAlteringBuilder | CreateTableBuilder | null = null @@ -69,22 +70,47 @@ export class UnderlyingTableFieldVisitor const c = this.tb.addColumn(field.id.value, "timestamp", (b) => b.defaultTo(sql`(CURRENT_TIMESTAMP)`).notNull()) this.addColumn(c) - const query = sql - .raw( - ` + if (this.dbProvider === "postgres") { + const query = sql + .raw( + ` +CREATE OR REPLACE FUNCTION update_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW."${field.id.value}" = now(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_customer_modtime_${tableName} BEFORE UPDATE ON ${tableName} FOR EACH ROW EXECUTE PROCEDURE update_modified_column(); +`, + ) + .compile(this.qb) + + this.#sql.push(query) + } else { + const query = sql + .raw( + ` CREATE TRIGGER IF NOT EXISTS update_at_update_${tableName} AFTER UPDATE ON \`${tableName}\` BEGIN update \`${tableName}\` SET ${field.id.value} = datetime('now') WHERE ${ID_TYPE} = NEW.${ID_TYPE}; END; `, - ) - .compile(this.qb) + ) + .compile(this.qb) - this.#sql.push(query) + this.#sql.push(query) + } } autoIncrement(field: AutoIncrementField): void { - const c = this.tb.addColumn(field.id.value, "integer", (b) => b.autoIncrement().primaryKey()) - this.addColumn(c) + if (this.dbProvider === "postgres") { + const c = this.tb.addColumn(field.id.value, "bigserial", (b) => b.primaryKey()) + this.addColumn(c) + } else { + const c = this.tb.addColumn(field.id.value, "integer", (b) => b.primaryKey().autoIncrement()) + this.addColumn(c) + } } createdAt(field: CreatedAtField): void { const c = this.tb.addColumn(field.id.value, "timestamp", (b) => b.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()) @@ -166,15 +192,19 @@ export class UnderlyingTableFieldVisitor const sql = this.qb.schema .createTable(joinTable.getTableName()) .ifNotExists() - .addColumn(joinTable.getSymmetricValueFieldId(), "varchar(10)", (b) => b.notNull()) - .addColumn(joinTable.getValueFieldId(), "varchar(10)", (b) => b.notNull()) - .addPrimaryKeyConstraint("primary_key", [joinTable.getSymmetricValueFieldId(), joinTable.getValueFieldId()]) + .addColumn(joinTable.getSymmetricValueFieldId(), "varchar(255)", (b) => b.notNull()) + .addColumn(joinTable.getValueFieldId(), "varchar(255)", (b) => b.notNull()) + .addPrimaryKeyConstraint(joinTable.getTableName() + "_primary_key", [ + joinTable.getSymmetricValueFieldId(), + joinTable.getValueFieldId(), + ]) .compile() this.addSql(sql) } rollup(field: RollupField): void {} checkbox(field: CheckboxField): void { - const c = this.tb.addColumn(field.id.value, "boolean", (b) => b.defaultTo(0).notNull()) + const defaultValue = this.dbProvider === "postgres" ? false : 0 + const c = this.tb.addColumn(field.id.value, "boolean", (b) => b.defaultTo(defaultValue).notNull()) this.addColumn(c) } duration(field: DurationField): void { @@ -200,6 +230,9 @@ export class UnderlyingTableFieldVisitor const type = getUnderlyingFormulaType(field.returnType) const c = this.tb.addColumn(field.id.value, type, (b) => { const column = b.generatedAlwaysAs(sql.raw(parsed)) + if (this.dbProvider === "postgres") { + return column.stored() + } return this.isNew ? column.stored() : column }) this.addColumn(c) diff --git a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts index 77e64331e..8269e4162 100644 --- a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts @@ -48,6 +48,7 @@ import type { import type { WithTableRLS } from "@undb/table/src/specifications/table-rls.specification" import { AlterTableBuilder, AlterTableColumnAlteringBuilder, CompiledQuery, CreateTableBuilder, sql } from "kysely" import type { IRecordQueryBuilder } from "../qb.type" +import type { IDatabaseFnUtil } from "../utils/fn.util" import { ConversionContext } from "./conversion/conversion.context" import { ConversionFactory } from "./conversion/conversion.factory" import { JoinTable } from "./reference/join-table" @@ -61,6 +62,8 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { public readonly table: UnderlyingTable, public readonly qb: IRecordQueryBuilder, public readonly context: IContext, + private readonly dbProvider: string, + private readonly dbFnUtil: IDatabaseFnUtil, ) { this.tb = qb.schema.alterTable(table.name) } @@ -119,7 +122,7 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { .updateTable(this.table.name) .where((eb) => eb.not(eb.or([eb(field.id.value, "is", null), eb(field.id.value, "=", "")]))) .set((eb) => ({ - [field.id.value]: eb.fn(`json_array`, [sql.raw(field.id.value)]), + [field.id.value]: eb.fn(this.dbFnUtil.jsonArray, [sql.raw(`${this.table.name}."${field.id.value}"`)]), })) .compile() @@ -138,7 +141,13 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { this.addSql(query) } - const fieldVisitor = new UnderlyingTableFieldUpdatedVisitor(this.qb, this.table, spec.previous, this.tb) + const fieldVisitor = new UnderlyingTableFieldUpdatedVisitor( + this.qb, + this.table, + spec.previous, + this.tb, + this.dbFnUtil, + ) spec.field.accept(fieldVisitor) this.addSql(...fieldVisitor.sql) } @@ -217,7 +226,7 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { withName(name: TableNameSpecification): void {} withSchema(schema: TableSchemaSpecification): void {} withNewField(schema: WithNewFieldSpecification): void { - const fieldVisitor = new UnderlyingTableFieldVisitor(this.qb, this.table, this.tb, false) + const fieldVisitor = new UnderlyingTableFieldVisitor(this.qb, this.table, this.tb, this.dbProvider, false) schema.field.accept(fieldVisitor) this.addSql(...fieldVisitor.sql) this.atb = fieldVisitor.atb diff --git a/packages/persistence/src/underlying/underlying-table.service.ts b/packages/persistence/src/underlying/underlying-table.service.ts index 8425612d8..f04fd966c 100644 --- a/packages/persistence/src/underlying/underlying-table.service.ts +++ b/packages/persistence/src/underlying/underlying-table.service.ts @@ -1,10 +1,12 @@ import { injectContext, type IContext } from "@undb/context" -import { singleton } from "@undb/di" +import { inject, singleton } from "@undb/di" import { createLogger } from "@undb/logger" import type { TableComositeSpecification, TableDo } from "@undb/table" import type { CompiledQuery } from "kysely" import type { ITxContext } from "../ctx.interface" import { injectTxCTX } from "../ctx.provider" +import { injectDbProvider } from "../db.provider" +import { DatabaseFnUtil, type IDatabaseFnUtil } from "../utils/fn.util" import { JoinTable } from "./reference/join-table" import { UnderlyingTable } from "./underlying-table" import { UnderlyingTableFieldVisitor } from "./underlying-table-field.visitor" @@ -16,6 +18,10 @@ export class UnderlyingTableService { @injectContext() private readonly context: IContext, @injectTxCTX() private readonly txContext: ITxContext, + @injectDbProvider() + private readonly dbProvider: string, + @inject(DatabaseFnUtil) + private readonly dbFnUtil: IDatabaseFnUtil, ) {} readonly logger = createLogger(UnderlyingTableService.name) @@ -28,7 +34,7 @@ export class UnderlyingTableService { .createTable(t.name) .ifNotExists() .$call((tb) => { - const visitor = new UnderlyingTableFieldVisitor(trx, t, tb, true) + const visitor = new UnderlyingTableFieldVisitor(trx, t, tb, this.dbProvider, true) for (const field of table.schema) { field.accept(visitor) } @@ -46,7 +52,7 @@ export class UnderlyingTableService { const t = new UnderlyingTable(table) const trx = this.txContext.getAnonymousTransaction() - const visitor = new UnderlyingTableSpecVisitor(t, trx, this.context) + const visitor = new UnderlyingTableSpecVisitor(t, trx, this.context, this.dbProvider, this.dbFnUtil) spec.accept(visitor) await visitor.execute() diff --git a/packages/persistence/src/underlying/underlying-table.util.ts b/packages/persistence/src/underlying/underlying-table.util.ts index 775e43690..bddc20b51 100644 --- a/packages/persistence/src/underlying/underlying-table.util.ts +++ b/packages/persistence/src/underlying/underlying-table.util.ts @@ -1,10 +1,11 @@ import type { DateRangeField, FieldType, IRollupFn } from "@undb/table" import type { ColumnDataType } from "kysely" import { match } from "ts-pattern" +import type { IDatabaseFnUtil } from "../utils/fn.util" -export function getRollupFn(fn: IRollupFn): string { +export function getRollupFn(fn: IRollupFn, dbFnUtil: IDatabaseFnUtil): string { return match(fn) - .with("lookup", () => "json_group_array") + .with("lookup", () => dbFnUtil.jsonGroupArray) .with("average", () => "avg") .with("sum", () => "sum") .with("count", () => "count") diff --git a/packages/persistence/src/utils/fn.util.ts b/packages/persistence/src/utils/fn.util.ts new file mode 100644 index 000000000..6db0a8310 --- /dev/null +++ b/packages/persistence/src/utils/fn.util.ts @@ -0,0 +1,32 @@ +import { singleton } from "@undb/di" +import { match } from "ts-pattern" +import { injectDbProvider } from "../db.provider" + +export interface IDatabaseFnUtil { + get jsonGroupArray(): string + get jsonObject(): string + get jsonArray(): string +} + +@singleton() +export class DatabaseFnUtil implements IDatabaseFnUtil { + constructor(@injectDbProvider() private readonly dbProvider: string) {} + + get jsonGroupArray() { + return match(this.dbProvider) + .with("postgres", () => "json_agg") + .otherwise(() => "json_group_array") + } + + get jsonObject() { + return match(this.dbProvider) + .with("postgres", () => "json_build_object") + .otherwise(() => "json_object") + } + + get jsonArray() { + return match(this.dbProvider) + .with("postgres", () => "json_build_array") + .otherwise(() => "json_array") + } +} diff --git a/packages/template/src/templates/everything.base.json b/packages/template/src/templates/everything.base.json index 0ced2f4b4..e96ef31b5 100644 --- a/packages/template/src/templates/everything.base.json +++ b/packages/template/src/templates/everything.base.json @@ -172,6 +172,20 @@ "option": { "fn": "{{formula_number1}} - {{formula_number2}}" } + }, + "ADD": { + "id": "formula_add", + "type": "formula", + "option": { + "fn": "ADD(1, 2)" + } + }, + "ADD Number1 Number2": { + "id": "formula_add_number1_number2", + "type": "formula", + "option": { + "fn": "ADD({{formula_number1}}, {{formula_number2}})" + } } }, "records": [ diff --git a/scripts/migrate.ts b/scripts/migrate.ts index 3751bc6da..88553425f 100644 --- a/scripts/migrate.ts +++ b/scripts/migrate.ts @@ -3,7 +3,7 @@ import fs from "node:fs" import path from "node:path" import url from "node:url" -const { default: journal } = await import("../apps/backend/drizzle/meta/_journal.json", { +const { default: journal } = await import("../apps/backend/drizzle/sqlite/meta/_journal.json", { with: { type: "json" }, }) @@ -28,7 +28,7 @@ for (let index = 0; index < journal.entries.length; index++) { console.log('(%d) Parsing migration tagged "%s"', index + 1, tag) - const filepath = path.resolve(root, "./apps/backend/drizzle", `${tag}.sql`) + const filepath = path.resolve(root, "./apps/backend/drizzle/sqlite", `${tag}.sql`) const migration_file = fs.readFileSync(filepath).toString() migrate.push({