diff --git a/apps/api/.dev.vars-example b/apps/api/.dev.vars-example index 598e44b..5120501 100644 --- a/apps/api/.dev.vars-example +++ b/apps/api/.dev.vars-example @@ -1,3 +1,8 @@ RESEND_API_KEY= BETTER_AUTH_SECRET="" -BETTER_AUTH_TRUSTED_ORIGINS="," \ No newline at end of file +BETTER_AUTH_TRUSTED_ORIGINS="," +BETTER_AUTH_BASE_URL=http://localhost:3000 +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= \ No newline at end of file diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts index 2b969bd..b60fea5 100644 --- a/apps/api/drizzle.config.ts +++ b/apps/api/drizzle.config.ts @@ -1,14 +1,42 @@ -import { defineConfig } from "drizzle-kit"; +import fs from "node:fs"; +import path from "node:path"; +import type { Config } from "drizzle-kit"; -export default defineConfig({ +const getLocalD1 = (): string => { + const wranglerDir = path.resolve(".wrangler"); + + try { + const files = fs.readdirSync(wranglerDir, { + encoding: "utf-8", + recursive: true, + }); + + const dbFile = files.find((f) => f.endsWith(".sqlite")); + + if (!dbFile) { + throw new Error(`No SQLite database found in ${wranglerDir}`); + } + + return path.resolve(wranglerDir, dbFile); + } catch { + return path.resolve(wranglerDir, "default.sqlite"); + } +}; + +export default { dialect: "sqlite", - driver: "d1-http", - out: "drizzle", schema: "./src/db/schema.ts", - // Only needed for drizzle studio - // dbCredentials: { - // accountId: Bun.env.DB_ACCOUNT_ID!, - // databaseId: Bun.env.DB_DATABASE_ID!, - // token: Bun.env.DB_TOKEN!, - // }, -}); + out: "./drizzle", + ...(!process.env.DEV_MODE + ? { + driver: "d1-http", + } + : {}), + ...(process.env.DEV_MODE + ? { + dbCredentials: { + url: getLocalD1(), + }, + } + : {}), +} satisfies Config; diff --git a/apps/api/drizzle/0000_busy_mandrill.sql b/apps/api/drizzle/0000_busy_mandrill.sql new file mode 100644 index 0000000..eada876 --- /dev/null +++ b/apps/api/drizzle/0000_busy_mandrill.sql @@ -0,0 +1,127 @@ +CREATE TABLE `accounts` ( + `id` text PRIMARY KEY NOT NULL, + `account_id` text NOT NULL, + `provider_id` text NOT NULL, + `user_id` text NOT NULL, + `access_token` text, + `refresh_token` text, + `id_token` text, + `access_token_expires_at` integer, + `refresh_token_expires_at` integer, + `scope` text, + `password` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `accounts_user_id_idx` ON `accounts` (`user_id`);--> statement-breakpoint +CREATE INDEX `provider_compound_idx` ON `accounts` (`provider_id`,`account_id`);--> statement-breakpoint +CREATE TABLE `invitations` ( + `id` text PRIMARY KEY NOT NULL, + `organization_id` text NOT NULL, + `email` text NOT NULL, + `role` text, + `status` text NOT NULL, + `expires_at` integer NOT NULL, + `inviter_id` text NOT NULL, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`inviter_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `org_email_idx` ON `invitations` (`organization_id`,`email`);--> statement-breakpoint +CREATE INDEX `invitations_expires_at_idx` ON `invitations` (`expires_at`);--> statement-breakpoint +CREATE TABLE `members` ( + `id` text PRIMARY KEY NOT NULL, + `organization_id` text NOT NULL, + `user_id` text NOT NULL, + `role` text NOT NULL, + `created_at` integer NOT NULL, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `org_user_idx` ON `members` (`organization_id`,`user_id`);--> statement-breakpoint +CREATE TABLE `organizations` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `slug` text, + `logo` text, + `plan` text DEFAULT 'free' NOT NULL, + `api_key` text NOT NULL, + `created_at` integer NOT NULL, + `metadata` text +); +--> statement-breakpoint +CREATE UNIQUE INDEX `organizations_slug_unique` ON `organizations` (`slug`);--> statement-breakpoint +CREATE UNIQUE INDEX `organizations_api_key_unique` ON `organizations` (`api_key`);--> statement-breakpoint +CREATE INDEX `slug_idx` ON `organizations` (`slug`);--> statement-breakpoint +CREATE INDEX `org_api_key_idx` ON `organizations` (`api_key`);--> statement-breakpoint +CREATE TABLE `project_settings` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `cache` integer DEFAULT true NOT NULL, + `context` integer DEFAULT true NOT NULL, + `temperature` real DEFAULT 0 NOT NULL, + `instructions` text, + `memory` integer DEFAULT true NOT NULL, + `grammar` integer DEFAULT true NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `project_idx` ON `project_settings` (`project_id`);--> statement-breakpoint +CREATE TABLE `projects` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `description` text, + `organization_id` text NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `org_idx` ON `projects` (`organization_id`);--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `expires_at` integer NOT NULL, + `token` text NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + `ip_address` text, + `user_agent` text, + `user_id` text NOT NULL, + `active_organization_id` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint +CREATE INDEX `user_id_idx` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE INDEX `token_idx` ON `sessions` (`token`);--> statement-breakpoint +CREATE INDEX `expires_at_idx` ON `sessions` (`expires_at`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL, + `email_verified` integer NOT NULL, + `image` text, + `api_key` text NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint +CREATE UNIQUE INDEX `users_api_key_unique` ON `users` (`api_key`);--> statement-breakpoint +CREATE INDEX `email_idx` ON `users` (`email`);--> statement-breakpoint +CREATE TABLE `verifications` ( + `id` text PRIMARY KEY NOT NULL, + `identifier` text NOT NULL, + `value` text NOT NULL, + `expires_at` integer NOT NULL, + `created_at` integer, + `updated_at` integer +); +--> statement-breakpoint +CREATE INDEX `identifier_idx` ON `verifications` (`identifier`);--> statement-breakpoint +CREATE INDEX `verifications_expires_at_idx` ON `verifications` (`expires_at`); \ No newline at end of file diff --git a/apps/api/drizzle/0000_swift_jocasta.sql b/apps/api/drizzle/0000_swift_jocasta.sql deleted file mode 100644 index 8c71238..0000000 --- a/apps/api/drizzle/0000_swift_jocasta.sql +++ /dev/null @@ -1,82 +0,0 @@ -CREATE TABLE `accounts` ( - `id` text PRIMARY KEY NOT NULL, - `account_id` text NOT NULL, - `provider_id` text NOT NULL, - `user_id` text NOT NULL, - `access_token` text, - `refresh_token` text, - `id_token` text, - `access_token_expires_at` integer, - `refresh_token_expires_at` integer, - `scope` text, - `password` text, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `invitations` ( - `id` text PRIMARY KEY NOT NULL, - `organization_id` text NOT NULL, - `email` text NOT NULL, - `role` text, - `status` text NOT NULL, - `expires_at` integer NOT NULL, - `inviter_id` text NOT NULL, - FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`inviter_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `members` ( - `id` text PRIMARY KEY NOT NULL, - `organization_id` text NOT NULL, - `user_id` text NOT NULL, - `role` text NOT NULL, - `created_at` integer NOT NULL, - FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `organizations` ( - `id` text PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `slug` text, - `logo` text, - `created_at` integer NOT NULL, - `metadata` text -); ---> statement-breakpoint -CREATE UNIQUE INDEX `organizations_slug_unique` ON `organizations` (`slug`);--> statement-breakpoint -CREATE TABLE `sessions` ( - `id` text PRIMARY KEY NOT NULL, - `expires_at` integer NOT NULL, - `token` text NOT NULL, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - `ip_address` text, - `user_agent` text, - `user_id` text NOT NULL, - `active_organization_id` text, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint -CREATE TABLE `users` ( - `id` text PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `email` text NOT NULL, - `email_verified` integer NOT NULL, - `image` text, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint -CREATE TABLE `verifications` ( - `id` text PRIMARY KEY NOT NULL, - `identifier` text NOT NULL, - `value` text NOT NULL, - `expires_at` integer NOT NULL, - `created_at` integer, - `updated_at` integer -); diff --git a/apps/api/drizzle/0001_steep_scrambler.sql b/apps/api/drizzle/0001_steep_scrambler.sql new file mode 100644 index 0000000..880280b --- /dev/null +++ b/apps/api/drizzle/0001_steep_scrambler.sql @@ -0,0 +1,2 @@ +ALTER TABLE `projects` ADD `slug` text NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `slug_org_idx` ON `projects` (`slug`,`organization_id`); \ No newline at end of file diff --git a/apps/api/drizzle/meta/0000_snapshot.json b/apps/api/drizzle/meta/0000_snapshot.json index 72b4a3c..40b7f5d 100644 --- a/apps/api/drizzle/meta/0000_snapshot.json +++ b/apps/api/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "c0d1b60a-b07c-4b62-b435-234305efdaf1", + "id": "bf4d6e59-42f1-4179-acaa-90ef17b87b48", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "accounts": { @@ -99,7 +99,23 @@ "autoincrement": false } }, - "indexes": {}, + "indexes": { + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "provider_compound_idx": { + "name": "provider_compound_idx", + "columns": [ + "provider_id", + "account_id" + ], + "isUnique": false + } + }, "foreignKeys": { "accounts_user_id_users_id_fk": { "name": "accounts_user_id_users_id_fk", @@ -111,7 +127,7 @@ "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" } }, @@ -172,7 +188,23 @@ "autoincrement": false } }, - "indexes": {}, + "indexes": { + "org_email_idx": { + "name": "org_email_idx", + "columns": [ + "organization_id", + "email" + ], + "isUnique": false + }, + "invitations_expires_at_idx": { + "name": "invitations_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, "foreignKeys": { "invitations_organization_id_organizations_id_fk": { "name": "invitations_organization_id_organizations_id_fk", @@ -184,7 +216,7 @@ "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" }, "invitations_inviter_id_users_id_fk": { @@ -197,7 +229,7 @@ "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" } }, @@ -244,7 +276,16 @@ "autoincrement": false } }, - "indexes": {}, + "indexes": { + "org_user_idx": { + "name": "org_user_idx", + "columns": [ + "organization_id", + "user_id" + ], + "isUnique": false + } + }, "foreignKeys": { "members_organization_id_organizations_id_fk": { "name": "members_organization_id_organizations_id_fk", @@ -256,7 +297,7 @@ "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" }, "members_user_id_users_id_fk": { @@ -269,7 +310,7 @@ "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" } }, @@ -308,6 +349,21 @@ "notNull": false, "autoincrement": false }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'free'" + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "integer", @@ -330,6 +386,27 @@ "slug" ], "isUnique": true + }, + "organizations_api_key_unique": { + "name": "organizations_api_key_unique", + "columns": [ + "api_key" + ], + "isUnique": true + }, + "slug_idx": { + "name": "slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "org_api_key_idx": { + "name": "org_api_key_idx", + "columns": [ + "api_key" + ], + "isUnique": false } }, "foreignKeys": {}, @@ -337,6 +414,187 @@ "uniqueConstraints": {}, "checkConstraints": {} }, + "project_settings": { + "name": "project_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache": { + "name": "cache", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "context": { + "name": "context", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "temperature": { + "name": "temperature", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory": { + "name": "memory", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "grammar": { + "name": "grammar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_idx": { + "name": "project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "project_settings_project_id_projects_id_fk": { + "name": "project_settings_project_id_projects_id_fk", + "tableFrom": "project_settings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "org_idx": { + "name": "org_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "projects_organization_id_organizations_id_fk": { + "name": "projects_organization_id_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, "sessions": { "name": "sessions", "columns": { @@ -411,6 +669,27 @@ "token" ], "isUnique": true + }, + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "token_idx": { + "name": "token_idx", + "columns": [ + "token" + ], + "isUnique": false + }, + "expires_at_idx": { + "name": "expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false } }, "foreignKeys": { @@ -424,7 +703,7 @@ "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" } }, @@ -470,6 +749,13 @@ "notNull": false, "autoincrement": false }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "integer", @@ -492,6 +778,20 @@ "email" ], "isUnique": true + }, + "users_api_key_unique": { + "name": "users_api_key_unique", + "columns": [ + "api_key" + ], + "isUnique": true + }, + "email_idx": { + "name": "email_idx", + "columns": [ + "email" + ], + "isUnique": false } }, "foreignKeys": {}, @@ -545,7 +845,22 @@ "autoincrement": false } }, - "indexes": {}, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + }, + "verifications_expires_at_idx": { + "name": "verifications_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, diff --git a/apps/api/drizzle/meta/0001_snapshot.json b/apps/api/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..3fefa34 --- /dev/null +++ b/apps/api/drizzle/meta/0001_snapshot.json @@ -0,0 +1,895 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "de0701f7-d397-4b99-b485-3f47dfd1fc4b", + "prevId": "bf4d6e59-42f1-4179-acaa-90ef17b87b48", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "provider_compound_idx": { + "name": "provider_compound_idx", + "columns": [ + "provider_id", + "account_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitations": { + "name": "invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "org_email_idx": { + "name": "org_email_idx", + "columns": [ + "organization_id", + "email" + ], + "isUnique": false + }, + "invitations_expires_at_idx": { + "name": "invitations_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitations_organization_id_organizations_id_fk": { + "name": "invitations_organization_id_organizations_id_fk", + "tableFrom": "invitations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_inviter_id_users_id_fk": { + "name": "invitations_inviter_id_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "members": { + "name": "members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "org_user_idx": { + "name": "org_user_idx", + "columns": [ + "organization_id", + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'free'" + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organizations_api_key_unique": { + "name": "organizations_api_key_unique", + "columns": [ + "api_key" + ], + "isUnique": true + }, + "slug_idx": { + "name": "slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "org_api_key_idx": { + "name": "org_api_key_idx", + "columns": [ + "api_key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_settings": { + "name": "project_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache": { + "name": "cache", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "context": { + "name": "context", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "temperature": { + "name": "temperature", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory": { + "name": "memory", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "grammar": { + "name": "grammar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_idx": { + "name": "project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "project_settings_project_id_projects_id_fk": { + "name": "project_settings_project_id_projects_id_fk", + "tableFrom": "project_settings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "org_idx": { + "name": "org_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "slug_org_idx": { + "name": "slug_org_idx", + "columns": [ + "slug", + "organization_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "projects_organization_id_organizations_id_fk": { + "name": "projects_organization_id_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "token_idx": { + "name": "token_idx", + "columns": [ + "token" + ], + "isUnique": false + }, + "expires_at_idx": { + "name": "expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_api_key_unique": { + "name": "users_api_key_unique", + "columns": [ + "api_key" + ], + "isUnique": true + }, + "email_idx": { + "name": "email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verifications": { + "name": "verifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + }, + "verifications_expires_at_idx": { + "name": "verifications_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index 1cb9cc1..f1a9b7d 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -5,8 +5,15 @@ { "idx": 0, "version": "6", - "when": 1735643219273, - "tag": "0000_swift_jocasta", + "when": 1736171596544, + "tag": "0000_busy_mandrill", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1736177427739, + "tag": "0001_steep_scrambler", "breakpoints": true } ] diff --git a/apps/api/locales/en.json b/apps/api/locales/en.json new file mode 100644 index 0000000..2d1dddd --- /dev/null +++ b/apps/api/locales/en.json @@ -0,0 +1,18 @@ +{ + "email": { + "welcome": { + "preview": "Welcome to Languine - Your Automated Localization Solution", + "greeting": "Hi %{name},", + "intro": "Thank you for signing up for Languine! We're excited to help you automate your application's localization.", + "feature1": "Seamless integration with your existing codebase", + "feature2": "Automated translation workflows", + "feature3": "Context-aware translations", + "feature4": "Translation management dashboard", + "support": "If you have any questions, feel free to reach out to us at %{email}", + "cta": { + "automate": "Start Automating", + "docs": "Read the Docs" + } + } + } +} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 69f9107..7443c1e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,7 +1,10 @@ { "name": "@languine/api", + "main": "src/index.ts", + "types": "src/index.ts", "scripts": { - "dev": "wrangler dev --port 3002", + "dev": "DEV_MODE=true wrangler dev --port 3002", + "email:dev": "email dev --port 3003 --dir src/emails/templates", "deploy": "wrangler deploy --minify", "typecheck": "tsc --noEmit", "clean": "rm -rf .turbo node_modules", @@ -10,24 +13,36 @@ "generate": "drizzle-kit generate", "drizzle:up": "drizzle-kit up", "amend": "drizzle-kit drop && drizzle-kit generate", - "studio": "drizzle-kit studio" + "studio": "DEV_MODE=true drizzle-kit studio" }, "dependencies": { "@hono/zod-validator": "^0.4.2", + "@paralleldrive/cuid2": "^2.2.2", + "@react-email/components": "0.0.31", + "@react-email/font": "^0.0.9", "@scalar/hono-api-reference": "^0.5.165", - "better-auth": "^1.1.7", + "better-auth": "^1.1.10", "drizzle-orm": "^0.38.3", - "hono": "^4.6.15", + "hono": "4.6.15", "hono-openapi": "^0.3.1", "kysely": "^0.27.5", "kysely-d1": "^0.3.0", + "react": "19.0.0", + "react-dom": "19.0.0", "resend": "^4.0.1", + "slugify": "^1.6.6", "zod": "^3.24.1", "zod-openapi": "^4.2.2" }, "devDependencies": { "@cloudflare/workers-types": "^4.20241230.0", + "@libsql/client": "^0.14.0", + "@types/react": "19.0.3", + "@types/react-dom": "19.0.2", + "add": "2.0.6", + "bun": "1.1.42", "drizzle-kit": "^0.30.1", + "react-email": "3.0.4", "wrangler": "^3.99.0" } } \ No newline at end of file diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index a9fa0de..e8f3350 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -1,133 +1,238 @@ -import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { createId } from "@paralleldrive/cuid2"; +import { + index, + integer, + real, + sqliteTable, + text, + uniqueIndex, +} from "drizzle-orm/sqlite-core"; -export const users = sqliteTable("users", { - id: text("id").primaryKey(), - name: text("name").notNull(), - email: text("email").notNull().unique(), - emailVerified: integer("email_verified", { mode: "boolean" }).notNull(), - image: text("image"), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), -}); - -export const sessions = sqliteTable("sessions", { - id: text("id").primaryKey(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), - token: text("token").notNull().unique(), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), - ipAddress: text("ip_address"), - userAgent: text("user_agent"), - userId: text("user_id") - .notNull() - .references(() => users.id), - activeOrganizationId: text("active_organization_id"), -}); - -export const accounts = sqliteTable("accounts", { - id: text("id").primaryKey(), - accountId: text("account_id").notNull(), - providerId: text("provider_id").notNull(), - userId: text("user_id") - .notNull() - .references(() => users.id), - accessToken: text("access_token"), - refreshToken: text("refresh_token"), - idToken: text("id_token"), - accessTokenExpiresAt: integer("access_token_expires_at", { - mode: "timestamp", +export const users = sqliteTable( + "users", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: integer("email_verified", { mode: "boolean" }).notNull(), + image: text("image"), + apiKey: text("api_key") + .notNull() + .unique() + .$defaultFn(() => `user_${createId()}`), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + }, + (table) => ({ + emailIdx: index("email_idx").on(table.email), }), - refreshTokenExpiresAt: integer("refresh_token_expires_at", { - mode: "timestamp", +); + +export const sessions = sqliteTable( + "sessions", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + token: text("token").notNull().unique(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + activeOrganizationId: text("active_organization_id"), + }, + (table) => ({ + userIdIdx: index("user_id_idx").on(table.userId), + tokenIdx: index("token_idx").on(table.token), + expiresAtIdx: index("expires_at_idx").on(table.expiresAt), }), - scope: text("scope"), - password: text("password"), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), -}); +); -export const verifications = sqliteTable("verifications", { - id: text("id").primaryKey(), - identifier: text("identifier").notNull(), - value: text("value").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), - createdAt: integer("created_at", { mode: "timestamp" }), - updatedAt: integer("updated_at", { mode: "timestamp" }), -}); +export const accounts = sqliteTable( + "accounts", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: integer("access_token_expires_at", { + mode: "timestamp", + }), + refreshTokenExpiresAt: integer("refresh_token_expires_at", { + mode: "timestamp", + }), + scope: text("scope"), + password: text("password"), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + }, + (table) => ({ + userIdIdx: index("accounts_user_id_idx").on(table.userId), + providerCompoundIdx: index("provider_compound_idx").on( + table.providerId, + table.accountId, + ), + }), +); -export const organizations = sqliteTable("organizations", { - id: text("id").primaryKey(), - name: text("name").notNull(), - slug: text("slug").unique(), - logo: text("logo"), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - metadata: text("metadata"), -}); +export const verifications = sqliteTable( + "verifications", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn( + () => new Date(), + ), + updatedAt: integer("updated_at", { mode: "timestamp" }), + }, + (table) => ({ + identifierIdx: index("identifier_idx").on(table.identifier), + expiresAtIdx: index("verifications_expires_at_idx").on(table.expiresAt), + }), +); -export const members = sqliteTable("members", { - id: text("id").primaryKey(), - organizationId: text("organization_id") - .notNull() - .references(() => organizations.id), - userId: text("user_id") - .notNull() - .references(() => users.id), - role: text("role").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), -}); +export const projects = sqliteTable( + "projects", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + name: text("name").notNull(), + slug: text("slug").notNull(), + description: text("description"), + organizationId: text("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }), + }, + (table) => ({ + orgIdx: index("org_idx").on(table.organizationId), + slugOrgIdx: uniqueIndex("slug_org_idx").on( + table.slug, + table.organizationId, + ), + }), +); -export const invitations = sqliteTable("invitations", { - id: text("id").primaryKey(), - organizationId: text("organization_id") - .notNull() - .references(() => organizations.id), - email: text("email").notNull(), - role: text("role"), - status: text("status").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), - inviterId: text("inviter_id") - .notNull() - .references(() => users.id), -}); +export const organizations = sqliteTable( + "organizations", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + name: text("name").notNull(), + slug: text("slug").unique(), + logo: text("logo"), + plan: text("plan", { enum: ["free", "pro"] }) + .notNull() + .default("free"), + apiKey: text("api_key") + .notNull() + .unique() + .$defaultFn(() => `org_${createId()}`), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + metadata: text("metadata"), + }, + (table) => ({ + slugIdx: index("slug_idx").on(table.slug), + apiKeyIdx: index("org_api_key_idx").on(table.apiKey), + }), +); -export const tokens = sqliteTable("tokens", { - id: text("id").primaryKey(), - name: text("name").notNull(), - token: text("token").notNull().unique(), - userId: text("user_id") - .notNull() - .references(() => users.id), - projectId: text("project_id") - .notNull() - .references(() => projects.id), - expiresAt: integer("expires_at", { mode: "timestamp" }), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - lastUsedAt: integer("last_used_at", { mode: "timestamp" }), -}); +export const members = sqliteTable( + "members", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + organizationId: text("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + role: text("role").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + }, + (table) => ({ + orgUserIdx: index("org_user_idx").on(table.organizationId, table.userId), + }), +); -export const projects = sqliteTable("projects", { - id: text("id").primaryKey(), - name: text("name").notNull(), - description: text("description"), - organizationId: text("organization_id") - .notNull() - .references(() => organizations.id), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), -}); +export const invitations = sqliteTable( + "invitations", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + organizationId: text("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + email: text("email").notNull(), + role: text("role"), + status: text("status").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + inviterId: text("inviter_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + }, + (table) => ({ + orgEmailIdx: index("org_email_idx").on(table.organizationId, table.email), + expiresAtIdx: index("invitations_expires_at_idx").on(table.expiresAt), + }), +); -export const projectSettings = sqliteTable("project_settings", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id), - cache: integer("cache", { mode: "boolean" }).notNull().default(true), - context: integer("context", { mode: "boolean" }).notNull().default(true), - temperature: real("temperature").notNull().default(0), - instructions: text("instructions"), - memory: integer("memory", { mode: "boolean" }).notNull().default(true), - grammar: integer("grammar", { mode: "boolean" }).notNull().default(true), - apiKey: text("api_key").notNull().unique(), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), -}); +export const projectSettings = sqliteTable( + "project_settings", + { + id: text() + .primaryKey() + .$defaultFn(() => createId()), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + cache: integer("cache", { mode: "boolean" }).notNull().default(true), + context: integer("context", { mode: "boolean" }).notNull().default(true), + temperature: real("temperature").notNull().default(0), + instructions: text("instructions"), + memory: integer("memory", { mode: "boolean" }).notNull().default(true), + grammar: integer("grammar", { mode: "boolean" }).notNull().default(true), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), + }, + (table) => ({ + projectIdx: index("project_idx").on(table.projectId), + }), +); diff --git a/apps/api/src/emails/components/footer.tsx b/apps/api/src/emails/components/footer.tsx new file mode 100644 index 0000000..a296c67 --- /dev/null +++ b/apps/api/src/emails/components/footer.tsx @@ -0,0 +1,55 @@ +import { getAppUrl } from "@/lib/envs"; +import { Column, Img, Link, Row, Section, Text } from "@react-email/components"; + +export function Footer() { + return ( +
+ Separator + + + Automated localization for your applications + + + + + + X + + + + + + GitHub + + + +
+ + © 2024 Languine. All rights reserved. This email was sent to you + because you signed up for Languine. + +
+
+ ); +} diff --git a/apps/api/src/emails/components/logo.tsx b/apps/api/src/emails/components/logo.tsx new file mode 100644 index 0000000..c708d4e --- /dev/null +++ b/apps/api/src/emails/components/logo.tsx @@ -0,0 +1,15 @@ +import { getAppUrl } from "@/lib/envs"; +import { Img, Section } from "@react-email/components"; + +export function Logo() { + return ( +
+ Languine Logo +
+ ); +} diff --git a/apps/api/src/emails/components/outline-button.tsx b/apps/api/src/emails/components/outline-button.tsx new file mode 100644 index 0000000..8ad7d24 --- /dev/null +++ b/apps/api/src/emails/components/outline-button.tsx @@ -0,0 +1,37 @@ +import { Button } from "@react-email/components"; + +export function OutlineButton({ + children, + className, + variant = "default", + href, +}: { + children: React.ReactNode; + className?: string; + variant?: "default" | "secondary"; + href?: string; +}) { + const isDefault = variant === "default"; + const backgroundColor = isDefault ? "#000000" : "#FFFFFF"; + const textColor = isDefault ? "#FFFFFF" : "#000000"; + + return ( +
+
+ +
+ ); +} diff --git a/apps/api/src/emails/templates/welcome.tsx b/apps/api/src/emails/templates/welcome.tsx new file mode 100644 index 0000000..170afc2 --- /dev/null +++ b/apps/api/src/emails/templates/welcome.tsx @@ -0,0 +1,103 @@ +import { + Body, + Container, + Font, + Head, + Html, + Link, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import { Footer } from "../components/footer"; +import { Logo } from "../components/logo"; +import { OutlineButton } from "../components/outline-button"; + +export default function WelcomeEmail({ + name = "Viktor", +}: { + name: string; +}) { + return ( + + + + + Welcome to Languine! Let's get started. + + + + + + + Hi {name}, welcome to Languine! + + + + We're excited to help you automate your localization workflow. + Here's what you can do with Languine: + + + + + Automatically detect and extract text that needs translation + + + + Translate your content into multiple languages with AI + + + + Keep translations in sync with your codebase + + + + Collaborate with your team in real-time + + +
+ + Start Automating + + + + Read the Docs + +
+ +
+ + If you have any questions, feel free to reach out to us at{" "} + + support@languine.ai + + +
+ +