diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 72b28cd01d..8aa542d1e0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,27 +18,22 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Install pnpm uses: pnpm/action-setup@v4 with: version: ${{ env.PNPM_VERSION }} - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - - name: Install dependencies run: pnpm install - - name: Build Next.js run: npx dotenvx run -f .env.test -- pnpm build test: runs-on: ubuntu-latest - services: postgres: image: postgres:15.4 @@ -53,75 +48,63 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Install pnpm uses: pnpm/action-setup@v4 with: version: ${{ env.PNPM_VERSION }} - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - - name: Install dependencies run: pnpm install - - # - name: Build Next.js for E2E tests - # run: pnpm build - # env: - # SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: Run linter run: pnpm lint - - name: Run type checker run: pnpm check-types - - name: Setup database schema run: pnpm db:test:push --force env: DATABASE_URL: postgres://postgres:password@localhost:5432/roo_code_test - - name: Run unit tests - run: pnpm test # -- --coverage + run: pnpm test env: DATABASE_URL: postgres://postgres:password@localhost:5432/roo_code_test - # - name: Upload coverage reports to Codecov - # uses: codecov/codecov-action@v4 - # env: - # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - # - name: Install Playwright (used for Storybook and E2E tests) - # run: npx playwright install --with-deps - - # - name: Run storybook tests - # run: pnpm test-storybook:ci - - # - name: Run E2E tests - # run: npx percy exec -- npm run test:e2e - # env: - # PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} - # CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - - # - uses: actions/upload-artifact@v4 - # if: always() - # with: - # name: test-results - # path: test-results/ - # retention-days: 7 - check: needs: [build, test] runs-on: ubuntu-latest steps: - name: NO-OP run: echo "All checks passed." + + db_migrate: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: [build, test] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - name: Install dependencies + run: pnpm install + - name: Migrate production database + run: npx drizzle-kit migrate + env: + DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }} diff --git a/src/db/enums.ts b/src/db/enums.ts index 9324ecbdb8..cd6080aedb 100644 --- a/src/db/enums.ts +++ b/src/db/enums.ts @@ -3,4 +3,5 @@ export enum AuditLogTargetType { DEFAULT_PARAMETERS = 2, MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected. CLOUD_SETTINGS = 4, + TASK_SHARE = 5, } diff --git a/src/db/migrations/0005_watery_sphinx.sql b/src/db/migrations/0005_watery_sphinx.sql new file mode 100644 index 0000000000..f15d5e3e6c --- /dev/null +++ b/src/db/migrations/0005_watery_sphinx.sql @@ -0,0 +1,19 @@ +CREATE TABLE "task_shares" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "task_id" text NOT NULL, + "organization_id" text NOT NULL, + "created_by_user_id" text NOT NULL, + "share_token" text NOT NULL, + "expires_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "task_shares_share_token_unique" UNIQUE("share_token") +); +--> statement-breakpoint +ALTER TABLE "task_shares" ADD CONSTRAINT "task_shares_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "task_shares" ADD CONSTRAINT "task_shares_created_by_user_id_users_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "task_shares_share_token_idx" ON "task_shares" USING btree ("share_token");--> statement-breakpoint +CREATE INDEX "task_shares_task_id_idx" ON "task_shares" USING btree ("task_id");--> statement-breakpoint +CREATE INDEX "task_shares_org_id_idx" ON "task_shares" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "task_shares_expires_at_idx" ON "task_shares" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "task_shares_created_by_user_id_idx" ON "task_shares" USING btree ("created_by_user_id"); \ No newline at end of file diff --git a/src/db/migrations/meta/0005_snapshot.json b/src/db/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000000..d1884ee91c --- /dev/null +++ b/src/db/migrations/meta/0005_snapshot.json @@ -0,0 +1,668 @@ +{ + "id": "9cf3a792-c153-4ad4-a3f5-0f8054abf28d", + "prevId": "5c95f3bf-5048-4ba1-9f71-2110879fc161", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_value": { + "name": "new_value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_organization_id_idx": { + "name": "audit_logs_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_target_idx": { + "name": "audit_logs_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_logs_organization_id_organizations_id_fk": { + "name": "audit_logs_organization_id_organizations_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_settings": { + "name": "organization_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "cloud_settings": { + "name": "cloud_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "default_settings": { + "name": "default_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "allow_list": { + "name": "allow_list", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"allowAll\":true,\"providers\":{}}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_settings_created_at_idx": { + "name": "organization_settings_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_settings_organization_id_organizations_id_fk": { + "name": "organization_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_settings", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizations_created_at_idx": { + "name": "organizations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_shares": { + "name": "task_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_shares_share_token_idx": { + "name": "task_shares_share_token_idx", + "columns": [ + { + "expression": "share_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_task_id_idx": { + "name": "task_shares_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_org_id_idx": { + "name": "task_shares_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_expires_at_idx": { + "name": "task_shares_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_created_by_user_id_idx": { + "name": "task_shares_created_by_user_id_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_shares_organization_id_organizations_id_fk": { + "name": "task_shares_organization_id_organizations_id_fk", + "tableFrom": "task_shares", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "task_shares_created_by_user_id_users_id_fk": { + "name": "task_shares_created_by_user_id_users_id_fk", + "tableFrom": "task_shares", + "tableTo": "users", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "task_shares_share_token_unique": { + "name": "task_shares_share_token_unique", + "nullsNotDistinct": false, + "columns": ["share_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_role": { + "name": "organization_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_organization_id_idx": { + "name": "users_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_organization_role_idx": { + "name": "users_organization_role_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_organization_id_organizations_id_fk": { + "name": "users_organization_id_organizations_id_fk", + "tableFrom": "users", + "tableTo": "organizations", + "columnsFrom": ["organization_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": {} + } +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 2f24d9b4af..7a24f53bf5 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1748555520729, "tag": "0004_overjoyed_bucky", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1748916566563, + "tag": "0005_watery_sphinx", + "breakpoints": true } ] } diff --git a/src/db/schema.ts b/src/db/schema.ts index 28cc11b9be..a5f5d7f9f9 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -76,6 +76,7 @@ export const orgs = pgTable( export const orgsRelations = relations(orgs, ({ many, one }) => ({ users: many(users), auditLogs: many(auditLogs), + taskShares: many(taskShares), orgSettings: one(orgSettings, { fields: [orgs.id], references: [orgSettings.orgId], @@ -159,3 +160,43 @@ export const auditLogsRelations = relations(auditLogs, ({ one }) => ({ references: [orgs.id], }), })); + +/** + * task_shares + */ + +export const taskShares = pgTable( + 'task_shares', + { + id: uuid('id').primaryKey().defaultRandom(), + taskId: text('task_id').notNull(), + orgId: text('organization_id') + .notNull() + .references(() => orgs.id), + createdByUserId: text('created_by_user_id') + .notNull() + .references(() => users.id), + shareToken: text('share_token').notNull().unique(), + expiresAt: timestamp('expires_at'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => [ + index('task_shares_share_token_idx').on(table.shareToken), + index('task_shares_task_id_idx').on(table.taskId), + index('task_shares_org_id_idx').on(table.orgId), + index('task_shares_expires_at_idx').on(table.expiresAt), + index('task_shares_created_by_user_id_idx').on(table.createdByUserId), + ], +); + +export const taskSharesRelations = relations(taskShares, ({ one }) => ({ + org: one(orgs, { + fields: [taskShares.orgId], + references: [orgs.id], + }), + createdByUser: one(users, { + fields: [taskShares.createdByUserId], + references: [users.id], + }), +}));