@odvi/create-dtt-framework 0.1.13 → 0.1.14
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/package.json +1 -1
- package/template/docs/framework/integrations.md +78 -0
- package/template/package.json +2 -1
- package/template/src/lib/aws-s3/client.ts +103 -0
- package/template/src/lib/framework.ts +47 -0
- package/template/src/lib/nextbank/client.ts +49 -24
- package/template/src/lib/snowflake/client.ts +37 -0
- package/template/src/lib/synology/client.ts +73 -0
package/package.json
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Reusable Integrations
|
|
2
|
+
|
|
3
|
+
The DTT Framework provides a unified interface to access all configured integrations through a single `framework` instance. This allows you to easily use third-party services in your application logic without manually managing connections or clients.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
You can import the `framework` instance from `@/lib/framework`.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { framework } from '@/lib/framework'
|
|
11
|
+
|
|
12
|
+
async function myFunction() {
|
|
13
|
+
// 1. Clerk Authentication
|
|
14
|
+
const user = await framework.clerk.users.getUser('user_id')
|
|
15
|
+
|
|
16
|
+
// 2. Supabase (Admin Access)
|
|
17
|
+
const { data } = await framework.supabase
|
|
18
|
+
.from('my_table')
|
|
19
|
+
.select('*')
|
|
20
|
+
|
|
21
|
+
// 3. Snowflake Query
|
|
22
|
+
const rows = await framework.snowflake.query('SELECT * FROM PUBLIC.USERS LIMIT 10')
|
|
23
|
+
|
|
24
|
+
// 4. NextBank API
|
|
25
|
+
const account = await framework.nextbank.get('/accounts/123')
|
|
26
|
+
await framework.nextbank.post('/transfers', { amount: 100 })
|
|
27
|
+
|
|
28
|
+
// 5. Synology File Operations
|
|
29
|
+
const files = await framework.synology.listFiles('/home/user')
|
|
30
|
+
const fileContent = await framework.synology.getFile('/home/user/doc.pdf')
|
|
31
|
+
|
|
32
|
+
// 6. AWS S3
|
|
33
|
+
await framework.s3.uploadFile('my-bucket', 'uploads/hello.txt', 'Hello World')
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Integration Details
|
|
38
|
+
|
|
39
|
+
### Clerk (`framework.clerk`)
|
|
40
|
+
Exposes the `@clerk/nextjs/server` client. Use this for server-side user management, organization handling, and session verification.
|
|
41
|
+
|
|
42
|
+
### Supabase (`framework.supabase`)
|
|
43
|
+
Exposes the Supabase Admin client (using `SUPABASE_SERVICE_ROLE_KEY`).
|
|
44
|
+
**Note:** This client has full admin privileges. Be careful when using it in client-facing code (it should only be used server-side).
|
|
45
|
+
|
|
46
|
+
### Snowflake (`framework.snowflake`)
|
|
47
|
+
Provides a simplified `query` method that handles connection pooling, execution, and cleanup automatically.
|
|
48
|
+
- `query(sqlText, binds?)`: Executes a SQL query.
|
|
49
|
+
|
|
50
|
+
### NextBank (`framework.nextbank`)
|
|
51
|
+
A wrapper around the NextBank API that automatically handles:
|
|
52
|
+
- Base URL injection
|
|
53
|
+
- Authentication headers
|
|
54
|
+
- JSON parsing
|
|
55
|
+
- Error handling
|
|
56
|
+
|
|
57
|
+
Methods:
|
|
58
|
+
- `get(endpoint)`
|
|
59
|
+
- `post(endpoint, body)`
|
|
60
|
+
- `put(endpoint, body)`
|
|
61
|
+
- `delete(endpoint)`
|
|
62
|
+
|
|
63
|
+
### Synology (`framework.synology`)
|
|
64
|
+
Provides methods to interact with Synology File Station.
|
|
65
|
+
- `listFiles(folderPath)`
|
|
66
|
+
- `getFile(filePath)`
|
|
67
|
+
- `deleteFile(filePath)`
|
|
68
|
+
|
|
69
|
+
### AWS S3 (`framework.s3`)
|
|
70
|
+
Wraps the AWS SDK S3 Client.
|
|
71
|
+
- `listBuckets()`
|
|
72
|
+
- `listFiles(bucket, prefix?)`
|
|
73
|
+
- `uploadFile(bucket, key, body, contentType?)`
|
|
74
|
+
- `getFile(bucket, key)`
|
|
75
|
+
- `deleteFile(bucket, key)`
|
|
76
|
+
- `getPresignedUrl(bucket, key, expiresIn?)`
|
|
77
|
+
- `getClient()`: Returns the raw AWS S3 Client for advanced operations.
|
|
78
|
+
|
package/template/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odvi/dtt-framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@aws-sdk/client-s3": "^3.958.0",
|
|
67
|
+
"@aws-sdk/s3-request-presigner": "^3.958.0",
|
|
67
68
|
"@clerk/nextjs": "^6.36.5",
|
|
68
69
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
69
70
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
S3Client,
|
|
3
|
+
ListBucketsCommand,
|
|
4
|
+
PutObjectCommand,
|
|
5
|
+
GetObjectCommand,
|
|
6
|
+
DeleteObjectCommand,
|
|
7
|
+
ListObjectsV2Command
|
|
8
|
+
} from '@aws-sdk/client-s3'
|
|
9
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
|
10
|
+
import { env } from '@/config/env'
|
|
11
|
+
|
|
12
|
+
export class S3IntegrationClient {
|
|
13
|
+
private client: S3Client
|
|
14
|
+
private region: string
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
this.region = env.AWS_REGION ?? 'us-east-1'
|
|
18
|
+
|
|
19
|
+
// Initialize with credentials from env
|
|
20
|
+
this.client = new S3Client({
|
|
21
|
+
region: this.region,
|
|
22
|
+
credentials: {
|
|
23
|
+
accessKeyId: env.AWS_ACCESS_KEY_ID ?? '',
|
|
24
|
+
secretAccessKey: env.AWS_ACCESS_SECRET_KEY ?? '',
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the underlying S3 Client
|
|
31
|
+
*/
|
|
32
|
+
getClient(): S3Client {
|
|
33
|
+
return this.client
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* List all buckets
|
|
38
|
+
*/
|
|
39
|
+
async listBuckets() {
|
|
40
|
+
const command = new ListBucketsCommand({})
|
|
41
|
+
return this.client.send(command)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* List files in a bucket
|
|
46
|
+
*/
|
|
47
|
+
async listFiles(bucket: string, prefix?: string) {
|
|
48
|
+
const command = new ListObjectsV2Command({
|
|
49
|
+
Bucket: bucket,
|
|
50
|
+
Prefix: prefix,
|
|
51
|
+
})
|
|
52
|
+
return this.client.send(command)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Upload a file to S3
|
|
57
|
+
*/
|
|
58
|
+
async uploadFile(bucket: string, key: string, body: Buffer | Uint8Array | Blob | string, contentType?: string) {
|
|
59
|
+
const command = new PutObjectCommand({
|
|
60
|
+
Bucket: bucket,
|
|
61
|
+
Key: key,
|
|
62
|
+
Body: body,
|
|
63
|
+
ContentType: contentType,
|
|
64
|
+
})
|
|
65
|
+
return this.client.send(command)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get a file from S3 (returns the raw output)
|
|
70
|
+
*/
|
|
71
|
+
async getFile(bucket: string, key: string) {
|
|
72
|
+
const command = new GetObjectCommand({
|
|
73
|
+
Bucket: bucket,
|
|
74
|
+
Key: key,
|
|
75
|
+
})
|
|
76
|
+
return this.client.send(command)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate a presigned URL for a file
|
|
81
|
+
*/
|
|
82
|
+
async getPresignedUrl(bucket: string, key: string, expiresIn = 3600) {
|
|
83
|
+
const command = new GetObjectCommand({
|
|
84
|
+
Bucket: bucket,
|
|
85
|
+
Key: key,
|
|
86
|
+
})
|
|
87
|
+
return getSignedUrl(this.client, command, { expiresIn })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Delete a file from S3
|
|
92
|
+
*/
|
|
93
|
+
async deleteFile(bucket: string, key: string) {
|
|
94
|
+
const command = new DeleteObjectCommand({
|
|
95
|
+
Bucket: bucket,
|
|
96
|
+
Key: key,
|
|
97
|
+
})
|
|
98
|
+
return this.client.send(command)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const s3Client = new S3IntegrationClient()
|
|
103
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { clerkClient } from '@clerk/nextjs/server'
|
|
2
|
+
import { supabaseAdmin } from './supabase/admin'
|
|
3
|
+
import { nextbankClient } from './nextbank/client'
|
|
4
|
+
import { synologyClient } from './synology/client'
|
|
5
|
+
import { snowflakeClient } from './snowflake/client'
|
|
6
|
+
import { s3Client } from './aws-s3/client'
|
|
7
|
+
|
|
8
|
+
export class Framework {
|
|
9
|
+
/**
|
|
10
|
+
* Clerk Authentication SDK
|
|
11
|
+
* Use this to manage users, sessions, and organizations.
|
|
12
|
+
*/
|
|
13
|
+
public clerk = clerkClient
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Supabase Admin Client (Service Role)
|
|
17
|
+
* Use this for database and storage operations with full privileges.
|
|
18
|
+
*/
|
|
19
|
+
public supabase = supabaseAdmin
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Snowflake Client
|
|
23
|
+
* Use this to execute SQL queries against your Snowflake data warehouse.
|
|
24
|
+
*/
|
|
25
|
+
public snowflake = snowflakeClient
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* AWS S3 Client
|
|
29
|
+
* Use this to manage files in AWS S3 buckets.
|
|
30
|
+
*/
|
|
31
|
+
public s3 = s3Client
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* NextBank Integration
|
|
35
|
+
* Use this to interact with the NextBank API.
|
|
36
|
+
*/
|
|
37
|
+
public nextbank = nextbankClient
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Synology Integration
|
|
41
|
+
* Use this to manage files on a Synology NAS.
|
|
42
|
+
*/
|
|
43
|
+
public synology = synologyClient
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const framework = new Framework()
|
|
47
|
+
|
|
@@ -17,22 +17,21 @@ class NextBankClient {
|
|
|
17
17
|
return { Authorization: `Basic ${token}` }
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
async
|
|
21
|
-
const url = `${this.apiUrl}/
|
|
22
|
-
console.log(`[NextBank] Pinging ${url}`)
|
|
20
|
+
private async request<T = any>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
21
|
+
const url = `${this.apiUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`
|
|
23
22
|
|
|
23
|
+
const headers = {
|
|
24
|
+
...this.getAuthHeader(),
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
'Accept': 'application/json',
|
|
27
|
+
'User-Agent': 'dtt-framework-client',
|
|
28
|
+
...options.headers,
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
try {
|
|
25
32
|
const response = await fetch(url, {
|
|
26
|
-
|
|
27
|
-
headers
|
|
28
|
-
...this.getAuthHeader(),
|
|
29
|
-
'Content-Type': 'application/json',
|
|
30
|
-
'Accept': 'application/json',
|
|
31
|
-
'User-Agent': 'dtt-framework-health-check',
|
|
32
|
-
},
|
|
33
|
-
body: JSON.stringify({
|
|
34
|
-
fingerprint: fingerprint,
|
|
35
|
-
}),
|
|
33
|
+
...options,
|
|
34
|
+
headers,
|
|
36
35
|
})
|
|
37
36
|
|
|
38
37
|
const contentType = response.headers.get('content-type')
|
|
@@ -40,28 +39,54 @@ class NextBankClient {
|
|
|
40
39
|
if (!response.ok) {
|
|
41
40
|
if (contentType && contentType.includes('text/html')) {
|
|
42
41
|
const text = await response.text()
|
|
43
|
-
|
|
44
|
-
console.error('1. NEXTBANK_API is pointing to the wrong server (e.g. Next.js app instead of NextBank API)')
|
|
45
|
-
console.error('2. The endpoint /management/status does not exist or does not support POST')
|
|
46
|
-
console.error(`[NextBank] Response preview: ${text.substring(0, 150)}...`)
|
|
47
|
-
throw new Error(`NextBank API error: ${response.status} (HTML response)`)
|
|
42
|
+
throw new Error(`NextBank API error: ${response.status} (HTML response): ${text.substring(0, 100)}...`)
|
|
48
43
|
}
|
|
49
44
|
throw new Error(`NextBank API error: ${response.status} ${response.statusText}`)
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
if (contentType && contentType.includes('application/json')) {
|
|
53
48
|
return await response.json()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If not JSON, return text if generic T is string, or try to return as is?
|
|
52
|
+
// For now, assume JSON or void.
|
|
53
|
+
const text = await response.text()
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(text)
|
|
56
|
+
} catch {
|
|
57
|
+
return text as unknown as T
|
|
59
58
|
}
|
|
60
59
|
} catch (error) {
|
|
61
|
-
console.error(
|
|
60
|
+
console.error(`[NextBank] Request to ${endpoint} failed:`, error)
|
|
62
61
|
throw error
|
|
63
62
|
}
|
|
64
63
|
}
|
|
64
|
+
|
|
65
|
+
async get<T = any>(endpoint: string): Promise<T> {
|
|
66
|
+
return this.request<T>(endpoint, { method: 'GET' })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async post<T = any>(endpoint: string, body: any): Promise<T> {
|
|
70
|
+
return this.request<T>(endpoint, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
body: JSON.stringify(body)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async put<T = any>(endpoint: string, body: any): Promise<T> {
|
|
77
|
+
return this.request<T>(endpoint, {
|
|
78
|
+
method: 'PUT',
|
|
79
|
+
body: JSON.stringify(body)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async delete<T = any>(endpoint: string): Promise<T> {
|
|
84
|
+
return this.request<T>(endpoint, { method: 'DELETE' })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async ping(fingerprint: string): Promise<{ status: string; timestamp: string }> {
|
|
88
|
+
return this.post('/management/status', { fingerprint })
|
|
89
|
+
}
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
export const nextbankClient = new NextBankClient()
|
|
@@ -100,3 +100,40 @@ export async function destroyConnection(connection: Connection): Promise<void> {
|
|
|
100
100
|
})
|
|
101
101
|
})
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
export class SnowflakeClient {
|
|
105
|
+
async query<T = unknown>(sqlText: string, binds?: Bind[]): Promise<T[]> {
|
|
106
|
+
let connection: Connection | undefined
|
|
107
|
+
try {
|
|
108
|
+
connection = await createSnowflakeConnection()
|
|
109
|
+
|
|
110
|
+
// Connect
|
|
111
|
+
await new Promise<void>((resolve, reject) => {
|
|
112
|
+
connection!.connect((err) => {
|
|
113
|
+
if (err) reject(err)
|
|
114
|
+
else resolve()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Execute
|
|
119
|
+
return await new Promise<T[]>((resolve, reject) => {
|
|
120
|
+
connection!.execute({
|
|
121
|
+
sqlText,
|
|
122
|
+
binds: binds as any,
|
|
123
|
+
complete: (err, stmt, rows) => {
|
|
124
|
+
if (err) reject(err)
|
|
125
|
+
else resolve((rows || []) as T[])
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
} finally {
|
|
130
|
+
if (connection) {
|
|
131
|
+
connection.destroy((err) => {
|
|
132
|
+
if (err) console.error('Error destroying Snowflake connection:', err)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const snowflakeClient = new SnowflakeClient()
|
|
@@ -74,6 +74,79 @@ class SynologyClient {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
async listFiles(folderPath: string) {
|
|
78
|
+
if (!this.sessionID) await this.login()
|
|
79
|
+
|
|
80
|
+
const params = new URLSearchParams({
|
|
81
|
+
api: 'SYNO.FileStation.List',
|
|
82
|
+
version: '2',
|
|
83
|
+
method: 'list',
|
|
84
|
+
folder_path: folderPath,
|
|
85
|
+
_sid: this.sessionID!
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const url = `${this.host}/webapi/entry.cgi?${params.toString()}`
|
|
89
|
+
const response = await this.fetchWithAgent(url)
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`Synology ListFiles error: ${response.status}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const data = await response.json()
|
|
96
|
+
if (!data.success) {
|
|
97
|
+
throw new Error(`Synology ListFiles failed: ${JSON.stringify(data)}`)
|
|
98
|
+
}
|
|
99
|
+
return data.data.files
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getFile(filePath: string) {
|
|
103
|
+
if (!this.sessionID) await this.login()
|
|
104
|
+
|
|
105
|
+
const params = new URLSearchParams({
|
|
106
|
+
api: 'SYNO.FileStation.Download',
|
|
107
|
+
version: '2',
|
|
108
|
+
method: 'download',
|
|
109
|
+
path: filePath,
|
|
110
|
+
mode: 'open',
|
|
111
|
+
_sid: this.sessionID!
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const url = `${this.host}/webapi/entry.cgi?${params.toString()}`
|
|
115
|
+
const response = await this.fetchWithAgent(url)
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`Synology GetFile error: ${response.status}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Return array buffer or blob?
|
|
122
|
+
return response.arrayBuffer()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async deleteFile(filePath: string) {
|
|
126
|
+
if (!this.sessionID) await this.login()
|
|
127
|
+
|
|
128
|
+
const params = new URLSearchParams({
|
|
129
|
+
api: 'SYNO.FileStation.Delete',
|
|
130
|
+
version: '2',
|
|
131
|
+
method: 'delete',
|
|
132
|
+
path: filePath,
|
|
133
|
+
_sid: this.sessionID!
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const url = `${this.host}/webapi/entry.cgi?${params.toString()}`
|
|
137
|
+
const response = await this.fetchWithAgent(url)
|
|
138
|
+
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new Error(`Synology DeleteFile error: ${response.status}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const data = await response.json()
|
|
144
|
+
if (!data.success) {
|
|
145
|
+
throw new Error(`Synology DeleteFile failed: ${JSON.stringify(data)}`)
|
|
146
|
+
}
|
|
147
|
+
return data
|
|
148
|
+
}
|
|
149
|
+
|
|
77
150
|
async ping(): Promise<{ status: string; timestamp: string; version?: string; sid?: string }> {
|
|
78
151
|
if (!this.host) {
|
|
79
152
|
throw new Error('Synology host not configured')
|