ta_data_mas 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/apps/api/.prettierrc +4 -0
- package/apps/api/README.md +98 -0
- package/apps/api/drizzle/0000_right_drax.sql +87 -0
- package/apps/api/drizzle/meta/0000_snapshot.json +588 -0
- package/apps/api/drizzle/meta/_journal.json +13 -0
- package/apps/api/drizzle.config.ts +12 -0
- package/apps/api/eslint.config.mjs +35 -0
- package/apps/api/nest-cli.json +8 -0
- package/apps/api/package.json +99 -0
- package/apps/api/src/app.controller.spec.ts +25 -0
- package/apps/api/src/app.controller.ts +9 -0
- package/apps/api/src/app.module.ts +37 -0
- package/apps/api/src/app.service.ts +8 -0
- package/apps/api/src/auth/auth.controller.ts +113 -0
- package/apps/api/src/auth/auth.module.ts +15 -0
- package/apps/api/src/auth/auth.service.ts +309 -0
- package/apps/api/src/auth/decorators/current-user-decorator.ts +10 -0
- package/apps/api/src/auth/decorators/require-org-role-decorator.ts +7 -0
- package/apps/api/src/auth/dto/login.dto.ts +10 -0
- package/apps/api/src/auth/dto/register.dto.ts +14 -0
- package/apps/api/src/auth/guards/jwt-auth-guard.ts +40 -0
- package/apps/api/src/auth/guards/org-role.guards.ts +60 -0
- package/apps/api/src/db/db.module.ts +9 -0
- package/apps/api/src/db/drizzle.service.ts +16 -0
- package/apps/api/src/db/schema/index.ts +6 -0
- package/apps/api/src/db/schema/organizations.ts +30 -0
- package/apps/api/src/db/schema/projects.ts +26 -0
- package/apps/api/src/db/schema/query-history.ts +16 -0
- package/apps/api/src/db/schema/storage-buckets.ts +17 -0
- package/apps/api/src/db/schema/storage-objects.ts +18 -0
- package/apps/api/src/db/schema/users.ts +14 -0
- package/apps/api/src/main.ts +51 -0
- package/apps/api/src/members/dto/invite-member-dto.ts +6 -0
- package/apps/api/src/members/dto/update-role.dto.ts +8 -0
- package/apps/api/src/members/invite.service.ts +147 -0
- package/apps/api/src/members/members.controller.ts +56 -0
- package/apps/api/src/members/members.module.ts +13 -0
- package/apps/api/src/members/members.service.ts +85 -0
- package/apps/api/src/orgs/dto/create-org.dto.ts +8 -0
- package/apps/api/src/orgs/orgs.controller.ts +27 -0
- package/apps/api/src/orgs/orgs.module.ts +12 -0
- package/apps/api/src/orgs/orgs.service.ts +86 -0
- package/apps/api/src/project-api/project-api.controller.ts +95 -0
- package/apps/api/src/project-api/project-api.module.ts +12 -0
- package/apps/api/src/project-api/project-api.service.ts +196 -0
- package/apps/api/src/project-api/project-key.guard.ts +45 -0
- package/apps/api/src/project-api/query-parser.ts +129 -0
- package/apps/api/src/project-auth/dto/magic-link.dto.ts +7 -0
- package/apps/api/src/project-auth/dto/signin.dto.ts +10 -0
- package/apps/api/src/project-auth/dto/signup.dto.ts +11 -0
- package/apps/api/src/project-auth/project-auth-dashboard.controller.ts +35 -0
- package/apps/api/src/project-auth/project-auth.controller.ts +111 -0
- package/apps/api/src/project-auth/project-auth.module.ts +13 -0
- package/apps/api/src/project-auth/project-auth.service.ts +440 -0
- package/apps/api/src/projects/dto/create-project.dto.ts +9 -0
- package/apps/api/src/projects/projects.controller.ts +34 -0
- package/apps/api/src/projects/projects.module.ts +12 -0
- package/apps/api/src/projects/projects.service.ts +136 -0
- package/apps/api/src/realtime/realtime-socket.type.ts +14 -0
- package/apps/api/src/realtime/realtime.controller.ts +63 -0
- package/apps/api/src/realtime/realtime.gateway.ts +150 -0
- package/apps/api/src/realtime/realtime.module.ts +14 -0
- package/apps/api/src/realtime/realtime.service.ts +101 -0
- package/apps/api/src/realtime/trigger.service.ts +95 -0
- package/apps/api/src/sql-editor/dto/execute-query.dto.ts +7 -0
- package/apps/api/src/sql-editor/sql-editor.controller.ts +28 -0
- package/apps/api/src/sql-editor/sql-editor.module.ts +12 -0
- package/apps/api/src/sql-editor/sql-editor.service.ts +154 -0
- package/apps/api/src/storage/project-storage.controller.ts +140 -0
- package/apps/api/src/storage/storage.controller.ts +118 -0
- package/apps/api/src/storage/storage.module.ts +15 -0
- package/apps/api/src/storage/storage.service.ts +191 -0
- package/apps/api/src/storage/uploadthing.ts +23 -0
- package/apps/api/src/table-editor/dto/alter-table.dto.ts +15 -0
- package/apps/api/src/table-editor/dto/create-table.dto.ts +54 -0
- package/apps/api/src/table-editor/table-editor.controller.ts +117 -0
- package/apps/api/src/table-editor/table-editor.module.ts +12 -0
- package/apps/api/src/table-editor/table-editor.service.ts +366 -0
- package/apps/api/test/app.e2e-spec.ts +29 -0
- package/apps/api/test/jest-e2e.json +9 -0
- package/apps/api/tsconfig.build.json +4 -0
- package/apps/api/tsconfig.json +25 -0
- package/apps/web/.env.example +12 -0
- package/apps/web/AGENTS.md +9 -0
- package/apps/web/CLAUDE.md +1 -0
- package/apps/web/README.md +36 -0
- package/apps/web/components.json +25 -0
- package/apps/web/eslint.config.mjs +18 -0
- package/apps/web/next.config.ts +11 -0
- package/apps/web/package.json +48 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/logo.svg +4 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/(dashboard)/layout.tsx +33 -0
- package/apps/web/src/app/(dashboard)/organizations/(list)/layout.tsx +7 -0
- package/apps/web/src/app/(dashboard)/organizations/(list)/new/page.tsx +26 -0
- package/apps/web/src/app/(dashboard)/organizations/(list)/page.tsx +44 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/api/page.tsx +13 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/layout.tsx +19 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/page.tsx +10 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/providers/page.tsx +19 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/url-configuration/page.tsx +24 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/users/page.tsx +13 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/database/page.tsx +29 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/realtime/page.tsx +28 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/sql/page.tsx +21 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/storage/page.tsx +28 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/layout.tsx +20 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/projects/page.tsx +62 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/members/page.tsx +28 -0
- package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/page.tsx +10 -0
- package/apps/web/src/app/(dashboard)/settings/page.tsx +12 -0
- package/apps/web/src/app/dashboard/page.tsx +14 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/globals.css +115 -0
- package/apps/web/src/app/layout.tsx +36 -0
- package/apps/web/src/app/login/page.tsx +199 -0
- package/apps/web/src/app/page.tsx +100 -0
- package/apps/web/src/app/register/page.tsx +218 -0
- package/apps/web/src/components/app-sidebar.tsx +301 -0
- package/apps/web/src/components/navbar.tsx +63 -0
- package/apps/web/src/components/topNav.tsx +68 -0
- package/apps/web/src/components/ui/avatar.tsx +112 -0
- package/apps/web/src/components/ui/badge.tsx +48 -0
- package/apps/web/src/components/ui/button.tsx +66 -0
- package/apps/web/src/components/ui/card.tsx +102 -0
- package/apps/web/src/components/ui/checkbox.tsx +32 -0
- package/apps/web/src/components/ui/dialog.tsx +168 -0
- package/apps/web/src/components/ui/dropdown-menu.tsx +269 -0
- package/apps/web/src/components/ui/field.tsx +238 -0
- package/apps/web/src/components/ui/input.tsx +19 -0
- package/apps/web/src/components/ui/label.tsx +24 -0
- package/apps/web/src/components/ui/resizable.tsx +50 -0
- package/apps/web/src/components/ui/scroll-area.tsx +55 -0
- package/apps/web/src/components/ui/select.tsx +192 -0
- package/apps/web/src/components/ui/separator.tsx +28 -0
- package/apps/web/src/components/ui/sheet.tsx +147 -0
- package/apps/web/src/components/ui/sidebar.tsx +703 -0
- package/apps/web/src/components/ui/skeleton.tsx +13 -0
- package/apps/web/src/components/ui/switch.tsx +33 -0
- package/apps/web/src/components/ui/table.tsx +116 -0
- package/apps/web/src/components/ui/tooltip.tsx +57 -0
- package/apps/web/src/features/api-docs/api-docs-client.tsx +227 -0
- package/apps/web/src/features/api-docs/api-docs-helpers.server.ts +86 -0
- package/apps/web/src/features/auth/action.ts +67 -0
- package/apps/web/src/features/auth/client.schema.ts +13 -0
- package/apps/web/src/features/auth/constants.ts +6 -0
- package/apps/web/src/features/auth/cookies.ts +86 -0
- package/apps/web/src/features/auth/server.schema.ts +11 -0
- package/apps/web/src/features/auth/sign-out.ts +17 -0
- package/apps/web/src/features/members/action.ts +66 -0
- package/apps/web/src/features/members/client.schema.ts +11 -0
- package/apps/web/src/features/members/constants.ts +8 -0
- package/apps/web/src/features/members/members-client.tsx +198 -0
- package/apps/web/src/features/members/members-helpers.server.ts +21 -0
- package/apps/web/src/features/members/server.schema.ts +19 -0
- package/apps/web/src/features/organization/action.ts +45 -0
- package/apps/web/src/features/organization/client.schema.ts +5 -0
- package/apps/web/src/features/organization/constants.tsx +3 -0
- package/apps/web/src/features/organization/create-org-form.tsx +66 -0
- package/apps/web/src/features/organization/organization-helpers.server.ts +35 -0
- package/apps/web/src/features/organization/server.schema.ts +10 -0
- package/apps/web/src/features/project-auth/project-auth-action.ts +78 -0
- package/apps/web/src/features/project-auth/project-auth-client.schema.ts +18 -0
- package/apps/web/src/features/project-auth/project-auth-constants.ts +8 -0
- package/apps/web/src/features/project-auth/project-auth-helpers.server.ts +39 -0
- package/apps/web/src/features/project-auth/project-auth-providers.tsx +222 -0
- package/apps/web/src/features/project-auth/project-auth-server.schema.ts +22 -0
- package/apps/web/src/features/project-auth/project-auth-shell.tsx +106 -0
- package/apps/web/src/features/project-auth/project-auth-url-config.tsx +151 -0
- package/apps/web/src/features/project-auth/project-auth-users.tsx +138 -0
- package/apps/web/src/features/projects/action.ts +46 -0
- package/apps/web/src/features/projects/client.schema.ts +9 -0
- package/apps/web/src/features/projects/constants.ts +6 -0
- package/apps/web/src/features/projects/create-project-modal.tsx +117 -0
- package/apps/web/src/features/projects/project-helpers.server.ts +38 -0
- package/apps/web/src/features/projects/server.schema.ts +10 -0
- package/apps/web/src/features/realtime/realtime-client.tsx +294 -0
- package/apps/web/src/features/realtime/realtime-helpers.server.ts +40 -0
- package/apps/web/src/features/sql-editor/sql-editor-client.tsx +324 -0
- package/apps/web/src/features/sql-editor/sql-editor-helpers.server.ts +22 -0
- package/apps/web/src/features/storage/storage-client.tsx +500 -0
- package/apps/web/src/features/storage/storage-helpers.server.ts +36 -0
- package/apps/web/src/features/table-editor/action.ts +97 -0
- package/apps/web/src/features/table-editor/add-column-dialog.tsx +184 -0
- package/apps/web/src/features/table-editor/client.schema.ts +36 -0
- package/apps/web/src/features/table-editor/create-table-drawer.tsx +542 -0
- package/apps/web/src/features/table-editor/server.schema.ts +41 -0
- package/apps/web/src/features/table-editor/table-editor-client.tsx +737 -0
- package/apps/web/src/features/table-editor/table-editor-helpers.server.ts +39 -0
- package/apps/web/src/hooks/use-mobile.ts +19 -0
- package/apps/web/src/lib/axios.ts +8 -0
- package/apps/web/src/lib/utils.ts +6 -0
- package/apps/web/src/proxy.ts +72 -0
- package/apps/web/src/server-utils/utils.ts +10 -0
- package/apps/web/tsconfig.json +34 -0
- package/package.json +22 -0
- package/packages/apiDatabase-js/package.json +39 -0
- package/packages/apiDatabase-js/src/auth/index.ts +121 -0
- package/packages/apiDatabase-js/src/client.ts +25 -0
- package/packages/apiDatabase-js/src/db/index.ts +15 -0
- package/packages/apiDatabase-js/src/db/query-builder.ts +194 -0
- package/packages/apiDatabase-js/src/index.ts +16 -0
- package/packages/apiDatabase-js/src/realtime/index.ts +84 -0
- package/packages/apiDatabase-js/src/storage/index.ts +130 -0
- package/packages/apiDatabase-js/tsconfig.json +13 -0
- package/packages/apiDatabase-js/tsdown.config.ts +20 -0
- package/packages/constants/package.json +6 -0
- package/packages/constants/src/index.ts +76 -0
- package/packages/tsconfig.json +13 -0
- package/packages/tsdown.config.ts +20 -0
- package/packages/types/package.json +6 -0
- package/packages/types/src/index.ts +254 -0
- package/pnpm-workspace.yaml +10 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Injectable,
|
|
4
|
+
NotFoundException,
|
|
5
|
+
} from '@nestjs/common';
|
|
6
|
+
import type {
|
|
7
|
+
FullQueryResults,
|
|
8
|
+
NeonQueryFunction,
|
|
9
|
+
} from '@neondatabase/serverless';
|
|
10
|
+
import { and, desc, eq } from 'drizzle-orm';
|
|
11
|
+
import { DrizzleService } from '../db/drizzle.service';
|
|
12
|
+
import { projects, organizations, queryHistory } from '../db/schema';
|
|
13
|
+
import type { QueryResult } from '@apiDatabase/types';
|
|
14
|
+
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class SqlEditorService {
|
|
17
|
+
constructor(private drizzle: DrizzleService) {}
|
|
18
|
+
|
|
19
|
+
private assertSafeIdentifier(name: string, label: string): void {
|
|
20
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
21
|
+
throw new BadRequestException(`Invalid ${label}: ${name}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private parseSingleStatement(querySql: string): string {
|
|
26
|
+
const statements = querySql
|
|
27
|
+
.split(';')
|
|
28
|
+
.map((part) => part.trim())
|
|
29
|
+
.filter((part) => part.length > 0);
|
|
30
|
+
|
|
31
|
+
if (statements.length === 0) {
|
|
32
|
+
throw new BadRequestException('SQL query cannot be empty');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (statements.length > 1) {
|
|
36
|
+
throw new BadRequestException(
|
|
37
|
+
'Only one SQL statement per run. Remove extra statements after a semicolon (e.g. a trailing SELECT).',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return statements[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private getNeonClient(): NeonQueryFunction<false, true> {
|
|
45
|
+
const client = (
|
|
46
|
+
this.drizzle.db as unknown as {
|
|
47
|
+
$client: NeonQueryFunction<false, true>;
|
|
48
|
+
}
|
|
49
|
+
).$client;
|
|
50
|
+
|
|
51
|
+
if (!client?.transaction) {
|
|
52
|
+
throw new Error('Neon HTTP client is not available');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return client;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private normalizePgResult(result: FullQueryResults<false>): {
|
|
59
|
+
rows: Record<string, unknown>[];
|
|
60
|
+
rowCount: number;
|
|
61
|
+
command: string;
|
|
62
|
+
} {
|
|
63
|
+
const rows = (result.rows ?? []) as Record<string, unknown>[];
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
rows,
|
|
67
|
+
rowCount: result.rowCount ?? rows.length,
|
|
68
|
+
command: result.command ?? 'SELECT',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async getProject(orgSlug: string, projectSlug: string) {
|
|
73
|
+
const [row] = await this.drizzle.db
|
|
74
|
+
.select({
|
|
75
|
+
id: projects.id,
|
|
76
|
+
dbSchema: projects.dbSchema,
|
|
77
|
+
})
|
|
78
|
+
.from(projects)
|
|
79
|
+
.innerJoin(organizations, eq(projects.orgId, organizations.id))
|
|
80
|
+
.where(
|
|
81
|
+
and(eq(organizations.slug, orgSlug), eq(projects.slug, projectSlug)),
|
|
82
|
+
)
|
|
83
|
+
.limit(1);
|
|
84
|
+
|
|
85
|
+
if (!row) throw new NotFoundException('Project not found');
|
|
86
|
+
return row;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async executeQuery(
|
|
90
|
+
orgSlug: string,
|
|
91
|
+
projectSlug: string,
|
|
92
|
+
querySql: string,
|
|
93
|
+
): Promise<QueryResult> {
|
|
94
|
+
const project = await this.getProject(orgSlug, projectSlug);
|
|
95
|
+
const statement = this.parseSingleStatement(querySql);
|
|
96
|
+
|
|
97
|
+
const normalised = statement.trim().toUpperCase();
|
|
98
|
+
const blocked = ['DROP DATABASE', 'DROP SCHEMA', 'TRUNCATE'];
|
|
99
|
+
if (blocked.some((b) => normalised.startsWith(b))) {
|
|
100
|
+
throw new BadRequestException('This statement is not allowed');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.assertSafeIdentifier(project.dbSchema, 'project schema');
|
|
104
|
+
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const neonSql = this.getNeonClient();
|
|
109
|
+
|
|
110
|
+
const results = await neonSql.transaction(
|
|
111
|
+
(txn) => [
|
|
112
|
+
txn`SET LOCAL search_path TO ${txn.unsafe(`"${project.dbSchema}", public`)}`,
|
|
113
|
+
txn`${txn.unsafe(statement)}`,
|
|
114
|
+
],
|
|
115
|
+
{ fullResults: true, readOnly: false },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const executionTimeMs = Date.now() - start;
|
|
119
|
+
const { rows, rowCount, command } = this.normalizePgResult(
|
|
120
|
+
results[results.length - 1],
|
|
121
|
+
);
|
|
122
|
+
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
123
|
+
|
|
124
|
+
await this.drizzle.db.insert(queryHistory).values({
|
|
125
|
+
projectId: project.id,
|
|
126
|
+
sql: querySql,
|
|
127
|
+
executionTimeMs,
|
|
128
|
+
rowCount,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
rows,
|
|
133
|
+
columns,
|
|
134
|
+
rowCount,
|
|
135
|
+
executionTimeMs,
|
|
136
|
+
command,
|
|
137
|
+
};
|
|
138
|
+
} catch (err: unknown) {
|
|
139
|
+
const message = err instanceof Error ? err.message : 'Query failed';
|
|
140
|
+
throw new BadRequestException(message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async getHistory(orgSlug: string, projectSlug: string) {
|
|
145
|
+
const project = await this.getProject(orgSlug, projectSlug);
|
|
146
|
+
|
|
147
|
+
return this.drizzle.db
|
|
148
|
+
.select()
|
|
149
|
+
.from(queryHistory)
|
|
150
|
+
.where(eq(queryHistory.projectId, project.id))
|
|
151
|
+
.orderBy(desc(queryHistory.createdAt))
|
|
152
|
+
.limit(50);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
All,
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Delete,
|
|
6
|
+
Get,
|
|
7
|
+
Next,
|
|
8
|
+
Param,
|
|
9
|
+
Post,
|
|
10
|
+
Req,
|
|
11
|
+
Res,
|
|
12
|
+
UseGuards,
|
|
13
|
+
} from '@nestjs/common';
|
|
14
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
15
|
+
import { IsNumber, IsString } from 'class-validator';
|
|
16
|
+
import { PROJECT_KEY_ROLES } from '@apiDatabase/constants';
|
|
17
|
+
import { createRouteHandler } from 'uploadthing/express';
|
|
18
|
+
import { StorageService } from './storage.service';
|
|
19
|
+
import { storageRouter } from './uploadthing';
|
|
20
|
+
import {
|
|
21
|
+
ProjectKeyGuard,
|
|
22
|
+
type ProjectKeyPayload,
|
|
23
|
+
} from '../project-api/project-key.guard';
|
|
24
|
+
import { ForbiddenException } from '@nestjs/common';
|
|
25
|
+
|
|
26
|
+
class SaveObjectDto {
|
|
27
|
+
@IsString()
|
|
28
|
+
name!: string;
|
|
29
|
+
|
|
30
|
+
@IsNumber()
|
|
31
|
+
size!: number;
|
|
32
|
+
|
|
33
|
+
@IsString()
|
|
34
|
+
type!: string;
|
|
35
|
+
|
|
36
|
+
@IsString()
|
|
37
|
+
utKey!: string;
|
|
38
|
+
|
|
39
|
+
@IsString()
|
|
40
|
+
url!: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const utHandler = createRouteHandler({ router: storageRouter });
|
|
44
|
+
|
|
45
|
+
@Controller('projects/:projectSlug/storage')
|
|
46
|
+
@UseGuards(ProjectKeyGuard)
|
|
47
|
+
export class ProjectStorageController {
|
|
48
|
+
constructor(private storageService: StorageService) {}
|
|
49
|
+
|
|
50
|
+
private getProjectKey(req: Request): ProjectKeyPayload {
|
|
51
|
+
return req['projectKey'] as ProjectKeyPayload;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private assertWriteAccess(req: Request): void {
|
|
55
|
+
const { role } = this.getProjectKey(req);
|
|
56
|
+
if (role !== PROJECT_KEY_ROLES.SERVICE_ROLE) {
|
|
57
|
+
throw new ForbiddenException(
|
|
58
|
+
'Write operations require the service role key',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Get('buckets/:bucketName/objects')
|
|
64
|
+
async getObjects(
|
|
65
|
+
@Req() req: Request,
|
|
66
|
+
@Param('projectSlug') projectSlug: string,
|
|
67
|
+
@Param('bucketName') bucketName: string,
|
|
68
|
+
) {
|
|
69
|
+
const { projectId } = this.getProjectKey(req);
|
|
70
|
+
await this.storageService.assertProjectSlug(projectId, projectSlug);
|
|
71
|
+
const bucket = await this.storageService.getBucketByName(
|
|
72
|
+
projectId,
|
|
73
|
+
bucketName,
|
|
74
|
+
);
|
|
75
|
+
return this.storageService.getObjects(bucket.id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@All('buckets/:bucketName/upload')
|
|
79
|
+
async handleUpload(
|
|
80
|
+
@Req() req: Request,
|
|
81
|
+
@Res() res: Response,
|
|
82
|
+
@Next() next: NextFunction,
|
|
83
|
+
@Param('projectSlug') projectSlug: string,
|
|
84
|
+
@Param('bucketName') bucketName: string,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
this.assertWriteAccess(req);
|
|
87
|
+
const { projectId } = this.getProjectKey(req);
|
|
88
|
+
await this.storageService.assertProjectSlug(projectId, projectSlug);
|
|
89
|
+
await this.storageService.getBucketByName(projectId, bucketName);
|
|
90
|
+
|
|
91
|
+
const originalUrl = req.url;
|
|
92
|
+
const queryIndex = originalUrl.indexOf('?');
|
|
93
|
+
const query = queryIndex >= 0 ? originalUrl.slice(queryIndex) : '';
|
|
94
|
+
req.url = `/${query}`;
|
|
95
|
+
utHandler(req, res, (err?: unknown) => {
|
|
96
|
+
req.url = originalUrl;
|
|
97
|
+
if (err) next(err);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Post('buckets/:bucketName/objects')
|
|
102
|
+
async saveObject(
|
|
103
|
+
@Req() req: Request,
|
|
104
|
+
@Param('projectSlug') projectSlug: string,
|
|
105
|
+
@Param('bucketName') bucketName: string,
|
|
106
|
+
@Body() file: SaveObjectDto,
|
|
107
|
+
) {
|
|
108
|
+
this.assertWriteAccess(req);
|
|
109
|
+
const { projectId } = this.getProjectKey(req);
|
|
110
|
+
await this.storageService.assertProjectSlug(projectId, projectSlug);
|
|
111
|
+
const bucket = await this.storageService.getBucketByName(
|
|
112
|
+
projectId,
|
|
113
|
+
bucketName,
|
|
114
|
+
);
|
|
115
|
+
return this.storageService.saveObject(bucket.id, file);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Delete('objects/:objectId')
|
|
119
|
+
async deleteObject(
|
|
120
|
+
@Req() req: Request,
|
|
121
|
+
@Param('projectSlug') projectSlug: string,
|
|
122
|
+
@Param('objectId') objectId: string,
|
|
123
|
+
) {
|
|
124
|
+
this.assertWriteAccess(req);
|
|
125
|
+
const { projectId } = this.getProjectKey(req);
|
|
126
|
+
await this.storageService.assertProjectSlug(projectId, projectSlug);
|
|
127
|
+
return this.storageService.deleteObject(objectId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Get('objects/:objectId/signed-url')
|
|
131
|
+
async getSignedUrl(
|
|
132
|
+
@Req() req: Request,
|
|
133
|
+
@Param('projectSlug') projectSlug: string,
|
|
134
|
+
@Param('objectId') objectId: string,
|
|
135
|
+
) {
|
|
136
|
+
const { projectId } = this.getProjectKey(req);
|
|
137
|
+
await this.storageService.assertProjectSlug(projectId, projectSlug);
|
|
138
|
+
return this.storageService.getSignedUrl(objectId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
All,
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Delete,
|
|
6
|
+
Get,
|
|
7
|
+
Next,
|
|
8
|
+
Param,
|
|
9
|
+
Post,
|
|
10
|
+
Req,
|
|
11
|
+
Res,
|
|
12
|
+
UseGuards,
|
|
13
|
+
} from '@nestjs/common';
|
|
14
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
15
|
+
import { createRouteHandler } from 'uploadthing/express';
|
|
16
|
+
import { IsNumber, IsEnum, IsString } from 'class-validator';
|
|
17
|
+
import { StorageService } from './storage.service';
|
|
18
|
+
import { storageRouter } from './uploadthing';
|
|
19
|
+
import { JwtAuthGuard } from '../auth/guards/jwt-auth-guard';
|
|
20
|
+
import { OrgRoleGuard } from '../auth/guards/org-role.guards';
|
|
21
|
+
import type { BucketAccess } from '@apiDatabase/types';
|
|
22
|
+
|
|
23
|
+
class CreateBucketDto {
|
|
24
|
+
@IsString()
|
|
25
|
+
name!: string;
|
|
26
|
+
|
|
27
|
+
@IsEnum(['public', 'private'])
|
|
28
|
+
access!: BucketAccess;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class SaveObjectDto {
|
|
32
|
+
@IsString()
|
|
33
|
+
name!: string;
|
|
34
|
+
|
|
35
|
+
@IsNumber()
|
|
36
|
+
size!: number;
|
|
37
|
+
|
|
38
|
+
@IsString()
|
|
39
|
+
type!: string;
|
|
40
|
+
|
|
41
|
+
@IsString()
|
|
42
|
+
utKey!: string;
|
|
43
|
+
|
|
44
|
+
@IsString()
|
|
45
|
+
url!: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const utHandler = createRouteHandler({ router: storageRouter });
|
|
49
|
+
|
|
50
|
+
@Controller('orgs/:slug/projects/:projectSlug/storage')
|
|
51
|
+
@UseGuards(JwtAuthGuard, OrgRoleGuard)
|
|
52
|
+
export class StorageController {
|
|
53
|
+
constructor(private storageService: StorageService) {}
|
|
54
|
+
|
|
55
|
+
@Get('buckets')
|
|
56
|
+
getBuckets(
|
|
57
|
+
@Param('slug') slug: string,
|
|
58
|
+
@Param('projectSlug') projectSlug: string,
|
|
59
|
+
) {
|
|
60
|
+
return this.storageService.getBuckets(slug, projectSlug);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Post('buckets')
|
|
64
|
+
createBucket(
|
|
65
|
+
@Param('slug') slug: string,
|
|
66
|
+
@Param('projectSlug') projectSlug: string,
|
|
67
|
+
@Body() dto: CreateBucketDto,
|
|
68
|
+
) {
|
|
69
|
+
return this.storageService.createBucket(
|
|
70
|
+
slug,
|
|
71
|
+
projectSlug,
|
|
72
|
+
dto.name,
|
|
73
|
+
dto.access,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Delete('buckets/:bucketId')
|
|
78
|
+
deleteBucket(@Param('bucketId') bucketId: string) {
|
|
79
|
+
return this.storageService.deleteBucket(bucketId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Get('buckets/:bucketId/objects')
|
|
83
|
+
getObjects(@Param('bucketId') bucketId: string) {
|
|
84
|
+
return this.storageService.getObjects(bucketId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// UploadThing upload endpoint — proxies the UT protocol (GET + POST)
|
|
88
|
+
@All('buckets/:bucketId/upload')
|
|
89
|
+
handleUpload(
|
|
90
|
+
@Req() req: Request,
|
|
91
|
+
@Res() res: Response,
|
|
92
|
+
@Next() next: NextFunction,
|
|
93
|
+
): void {
|
|
94
|
+
const originalUrl = req.url;
|
|
95
|
+
const queryIndex = originalUrl.indexOf('?');
|
|
96
|
+
const query = queryIndex >= 0 ? originalUrl.slice(queryIndex) : '';
|
|
97
|
+
req.url = `/${query}`;
|
|
98
|
+
utHandler(req, res, (err?: unknown) => {
|
|
99
|
+
req.url = originalUrl;
|
|
100
|
+
if (err) next(err);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Post('buckets/:bucketId/objects')
|
|
105
|
+
saveObject(@Param('bucketId') bucketId: string, @Body() file: SaveObjectDto) {
|
|
106
|
+
return this.storageService.saveObject(bucketId, file);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Delete('objects/:objectId')
|
|
110
|
+
deleteObject(@Param('objectId') objectId: string) {
|
|
111
|
+
return this.storageService.deleteObject(objectId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Get('objects/:objectId/signed-url')
|
|
115
|
+
getSignedUrl(@Param('objectId') objectId: string) {
|
|
116
|
+
return this.storageService.getSignedUrl(objectId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { StorageService } from './storage.service';
|
|
3
|
+
import { StorageController } from './storage.controller';
|
|
4
|
+
import { AuthModule } from '../auth/auth.module';
|
|
5
|
+
import { ProjectStorageController } from './project-storage.controller';
|
|
6
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
7
|
+
import { ProjectKeyGuard } from 'src/project-api/project-key.guard';
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [AuthModule, JwtModule.register({})],
|
|
11
|
+
|
|
12
|
+
providers: [StorageService, ProjectKeyGuard],
|
|
13
|
+
controllers: [StorageController, ProjectStorageController],
|
|
14
|
+
})
|
|
15
|
+
export class StorageModule {}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
ForbiddenException,
|
|
4
|
+
Injectable,
|
|
5
|
+
NotFoundException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { and, eq } from 'drizzle-orm';
|
|
8
|
+
import { UTApi } from 'uploadthing/server';
|
|
9
|
+
import { DrizzleService } from '../db/drizzle.service';
|
|
10
|
+
import {
|
|
11
|
+
projects,
|
|
12
|
+
organizations,
|
|
13
|
+
storageBuckets,
|
|
14
|
+
storageObjects,
|
|
15
|
+
} from '../db/schema';
|
|
16
|
+
import type { BucketAccess } from '@apiDatabase/types';
|
|
17
|
+
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class StorageService {
|
|
20
|
+
private utapi = new UTApi();
|
|
21
|
+
|
|
22
|
+
constructor(private drizzle: DrizzleService) {}
|
|
23
|
+
|
|
24
|
+
private async getProject(orgSlug: string, projectSlug: string) {
|
|
25
|
+
const [row] = await this.drizzle.db
|
|
26
|
+
.select({ id: projects.id })
|
|
27
|
+
.from(projects)
|
|
28
|
+
.innerJoin(organizations, eq(projects.orgId, organizations.id))
|
|
29
|
+
.where(
|
|
30
|
+
and(eq(organizations.slug, orgSlug), eq(projects.slug, projectSlug)),
|
|
31
|
+
)
|
|
32
|
+
.limit(1);
|
|
33
|
+
|
|
34
|
+
if (!row) throw new NotFoundException('Project not found');
|
|
35
|
+
return row;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getBuckets(orgSlug: string, projectSlug: string) {
|
|
39
|
+
const project = await this.getProject(orgSlug, projectSlug);
|
|
40
|
+
return this.drizzle.db
|
|
41
|
+
.select()
|
|
42
|
+
.from(storageBuckets)
|
|
43
|
+
.where(eq(storageBuckets.projectId, project.id));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async createBucket(
|
|
47
|
+
orgSlug: string,
|
|
48
|
+
projectSlug: string,
|
|
49
|
+
name: string,
|
|
50
|
+
access: BucketAccess,
|
|
51
|
+
) {
|
|
52
|
+
const project = await this.getProject(orgSlug, projectSlug);
|
|
53
|
+
|
|
54
|
+
const trimmed = name.trim();
|
|
55
|
+
if (!trimmed) {
|
|
56
|
+
throw new BadRequestException('Bucket name is required');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [bucket] = await this.drizzle.db
|
|
60
|
+
.insert(storageBuckets)
|
|
61
|
+
.values({ projectId: project.id, name: trimmed, access })
|
|
62
|
+
.returning();
|
|
63
|
+
|
|
64
|
+
return bucket;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async deleteBucket(bucketId: string) {
|
|
68
|
+
const objects = await this.drizzle.db
|
|
69
|
+
.select({ utKey: storageObjects.utKey })
|
|
70
|
+
.from(storageObjects)
|
|
71
|
+
.where(eq(storageObjects.bucketId, bucketId));
|
|
72
|
+
|
|
73
|
+
if (objects.length > 0) {
|
|
74
|
+
await this.utapi.deleteFiles(objects.map((o) => o.utKey));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await this.drizzle.db
|
|
78
|
+
.delete(storageBuckets)
|
|
79
|
+
.where(eq(storageBuckets.id, bucketId));
|
|
80
|
+
|
|
81
|
+
return { message: 'Bucket deleted' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getObjects(bucketId: string) {
|
|
85
|
+
return this.drizzle.db
|
|
86
|
+
.select()
|
|
87
|
+
.from(storageObjects)
|
|
88
|
+
.where(eq(storageObjects.bucketId, bucketId));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async saveObject(
|
|
92
|
+
bucketId: string,
|
|
93
|
+
file: {
|
|
94
|
+
name: string;
|
|
95
|
+
size: number;
|
|
96
|
+
type: string;
|
|
97
|
+
utKey: string;
|
|
98
|
+
url: string;
|
|
99
|
+
},
|
|
100
|
+
) {
|
|
101
|
+
const [bucket] = await this.drizzle.db
|
|
102
|
+
.select()
|
|
103
|
+
.from(storageBuckets)
|
|
104
|
+
.where(eq(storageBuckets.id, bucketId))
|
|
105
|
+
.limit(1);
|
|
106
|
+
|
|
107
|
+
if (!bucket) throw new NotFoundException('Bucket not found');
|
|
108
|
+
|
|
109
|
+
const url = bucket.access === 'public' ? file.url : '';
|
|
110
|
+
|
|
111
|
+
const [object] = await this.drizzle.db
|
|
112
|
+
.insert(storageObjects)
|
|
113
|
+
.values({
|
|
114
|
+
bucketId,
|
|
115
|
+
name: file.name,
|
|
116
|
+
size: file.size,
|
|
117
|
+
mimeType: file.type,
|
|
118
|
+
utKey: file.utKey,
|
|
119
|
+
url,
|
|
120
|
+
})
|
|
121
|
+
.returning();
|
|
122
|
+
|
|
123
|
+
return object;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async deleteObject(objectId: string) {
|
|
127
|
+
const [object] = await this.drizzle.db
|
|
128
|
+
.select()
|
|
129
|
+
.from(storageObjects)
|
|
130
|
+
.where(eq(storageObjects.id, objectId))
|
|
131
|
+
.limit(1);
|
|
132
|
+
|
|
133
|
+
if (!object) throw new NotFoundException('File not found');
|
|
134
|
+
|
|
135
|
+
await this.utapi.deleteFiles(object.utKey);
|
|
136
|
+
|
|
137
|
+
await this.drizzle.db
|
|
138
|
+
.delete(storageObjects)
|
|
139
|
+
.where(eq(storageObjects.id, objectId));
|
|
140
|
+
|
|
141
|
+
return { message: 'File deleted' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async getSignedUrl(objectId: string) {
|
|
145
|
+
const [object] = await this.drizzle.db
|
|
146
|
+
.select()
|
|
147
|
+
.from(storageObjects)
|
|
148
|
+
.where(eq(storageObjects.id, objectId))
|
|
149
|
+
.limit(1);
|
|
150
|
+
|
|
151
|
+
if (!object) throw new NotFoundException('File not found');
|
|
152
|
+
|
|
153
|
+
const { ufsUrl } = await this.utapi.generateSignedURL(object.utKey, {
|
|
154
|
+
expiresIn: 3600,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return { url: ufsUrl };
|
|
158
|
+
}
|
|
159
|
+
async assertProjectSlug(
|
|
160
|
+
projectId: string,
|
|
161
|
+
projectSlug: string,
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const [project] = await this.drizzle.db
|
|
164
|
+
.select({ slug: projects.slug })
|
|
165
|
+
.from(projects)
|
|
166
|
+
.where(eq(projects.id, projectId))
|
|
167
|
+
.limit(1);
|
|
168
|
+
|
|
169
|
+
if (!project) throw new NotFoundException('Project not found');
|
|
170
|
+
|
|
171
|
+
if (project.slug !== projectSlug) {
|
|
172
|
+
throw new ForbiddenException('API key does not match this project URL');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async getBucketByName(projectId: string, bucketName: string) {
|
|
177
|
+
const [bucket] = await this.drizzle.db
|
|
178
|
+
.select()
|
|
179
|
+
.from(storageBuckets)
|
|
180
|
+
.where(
|
|
181
|
+
and(
|
|
182
|
+
eq(storageBuckets.projectId, projectId),
|
|
183
|
+
eq(storageBuckets.name, bucketName),
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
.limit(1);
|
|
187
|
+
|
|
188
|
+
if (!bucket) throw new NotFoundException('Bucket not found');
|
|
189
|
+
return bucket;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createUploadthing, type FileRouter } from 'uploadthing/express';
|
|
2
|
+
import { FileRoute } from 'uploadthing/types';
|
|
3
|
+
|
|
4
|
+
const f = createUploadthing();
|
|
5
|
+
|
|
6
|
+
export const storageRouter: {
|
|
7
|
+
bucketUploader: FileRoute<{
|
|
8
|
+
input: undefined;
|
|
9
|
+
output: null;
|
|
10
|
+
errorShape: any;
|
|
11
|
+
}>;
|
|
12
|
+
} = {
|
|
13
|
+
bucketUploader: f({
|
|
14
|
+
blob: {
|
|
15
|
+
maxFileSize: '512MB',
|
|
16
|
+
maxFileCount: 10,
|
|
17
|
+
},
|
|
18
|
+
}).onUploadComplete((data) => {
|
|
19
|
+
console.log('UploadThing upload complete:', data.file.name);
|
|
20
|
+
}),
|
|
21
|
+
} satisfies FileRouter;
|
|
22
|
+
|
|
23
|
+
export type OurStorageRouter = typeof storageRouter;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IsIn, IsOptional, IsString } from 'class-validator';
|
|
2
|
+
import { COLUMN_TYPES } from '@apiDatabase/constants';
|
|
3
|
+
import type { ColumnType } from '@apiDatabase/types';
|
|
4
|
+
|
|
5
|
+
export class AddColumnDto {
|
|
6
|
+
@IsString()
|
|
7
|
+
name!: string;
|
|
8
|
+
|
|
9
|
+
@IsIn(COLUMN_TYPES)
|
|
10
|
+
type!: ColumnType;
|
|
11
|
+
|
|
12
|
+
@IsOptional()
|
|
13
|
+
@IsString()
|
|
14
|
+
defaultValue?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IsArray,
|
|
3
|
+
IsBoolean,
|
|
4
|
+
IsIn,
|
|
5
|
+
IsOptional,
|
|
6
|
+
IsString,
|
|
7
|
+
MinLength,
|
|
8
|
+
ValidateNested,
|
|
9
|
+
} from 'class-validator';
|
|
10
|
+
import { Type } from 'class-transformer';
|
|
11
|
+
import { COLUMN_TYPES } from '@apiDatabase/constants';
|
|
12
|
+
import type {
|
|
13
|
+
CreateTableInput,
|
|
14
|
+
CreateColumnInput,
|
|
15
|
+
ColumnType,
|
|
16
|
+
} from '@apiDatabase/types';
|
|
17
|
+
|
|
18
|
+
export class CreateColumnDto implements CreateColumnInput {
|
|
19
|
+
@IsString()
|
|
20
|
+
@MinLength(1)
|
|
21
|
+
name!: string;
|
|
22
|
+
|
|
23
|
+
@IsIn(COLUMN_TYPES)
|
|
24
|
+
type!: ColumnType;
|
|
25
|
+
|
|
26
|
+
@IsBoolean()
|
|
27
|
+
isNullable!: boolean;
|
|
28
|
+
|
|
29
|
+
@IsBoolean()
|
|
30
|
+
isPrimaryKey!: boolean;
|
|
31
|
+
|
|
32
|
+
@IsString()
|
|
33
|
+
@IsOptional()
|
|
34
|
+
defaultValue?: string;
|
|
35
|
+
|
|
36
|
+
@IsString()
|
|
37
|
+
@IsOptional()
|
|
38
|
+
foreignKeyTable?: string;
|
|
39
|
+
|
|
40
|
+
@IsString()
|
|
41
|
+
@IsOptional()
|
|
42
|
+
foreignKeyColumn?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class CreateTableDto implements CreateTableInput {
|
|
46
|
+
@IsString()
|
|
47
|
+
@MinLength(1)
|
|
48
|
+
name!: string;
|
|
49
|
+
|
|
50
|
+
@IsArray()
|
|
51
|
+
@ValidateNested({ each: true })
|
|
52
|
+
@Type(() => CreateColumnDto)
|
|
53
|
+
columns!: CreateColumnDto[];
|
|
54
|
+
}
|