drizzle-cube 0.1.0 → 0.1.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 +407 -0
- package/dist/adapters/hono/index.d.ts +49 -0
- package/dist/adapters/hono/index.js +169 -2
- package/dist/adapters/types.d.ts +41 -0
- package/dist/server/index.d.ts +1977 -76
- package/dist/server/index.js +3414 -485
- package/package.json +25 -4
package/README.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# 🐲 Drizzle Cube
|
|
2
|
+
|
|
3
|
+
**Drizzle ORM-first semantic layer with Cube.js compatibility**
|
|
4
|
+
|
|
5
|
+
Transform your Drizzle schema into a powerful, type-safe analytics platform with SQL injection protection and full TypeScript support.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/drizzle-cube)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://orm.drizzle.team/)
|
|
10
|
+
[](https://choosealicense.com/licenses/mit/)
|
|
11
|
+
|
|
12
|
+
## Why Drizzle Cube?
|
|
13
|
+
|
|
14
|
+
🔒 **SQL Injection Proof** - All queries use Drizzle's parameterized SQL
|
|
15
|
+
🛡️ **Type Safe** - Full TypeScript inference from your database schema
|
|
16
|
+
⚡ **Performance** - Prepared statements and query optimization
|
|
17
|
+
🧩 **Cube.js Compatible** - Works with existing Cube.js React components
|
|
18
|
+
🎯 **Zero Config** - Infer cube definitions from your Drizzle schema
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install drizzle-cube drizzle-orm
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Define Your Schema
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// schema.ts
|
|
32
|
+
import { pgTable, text, integer, boolean } from 'drizzle-orm/pg-core'
|
|
33
|
+
|
|
34
|
+
export const employees = pgTable('employees', {
|
|
35
|
+
id: integer('id').primaryKey(),
|
|
36
|
+
name: text('name').notNull(),
|
|
37
|
+
email: text('email'),
|
|
38
|
+
active: boolean('active').default(true),
|
|
39
|
+
departmentId: integer('department_id'),
|
|
40
|
+
organisationId: integer('organisation_id').notNull(),
|
|
41
|
+
salary: integer('salary')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const departments = pgTable('departments', {
|
|
45
|
+
id: integer('id').primaryKey(),
|
|
46
|
+
name: text('name').notNull(),
|
|
47
|
+
organisationId: integer('organisation_id').notNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
export const schema = { employees, departments }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Create Type-Safe Cubes
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// cubes.ts
|
|
57
|
+
import { defineCube } from 'drizzle-cube/server'
|
|
58
|
+
import { eq } from 'drizzle-orm'
|
|
59
|
+
import { schema } from './schema'
|
|
60
|
+
|
|
61
|
+
export const employeesCube = defineCube('Employees', {
|
|
62
|
+
title: 'Employee Analytics',
|
|
63
|
+
|
|
64
|
+
// Use Drizzle query structure for type safety
|
|
65
|
+
sql: (ctx) => ({
|
|
66
|
+
from: schema.employees,
|
|
67
|
+
joins: [
|
|
68
|
+
{
|
|
69
|
+
table: schema.departments,
|
|
70
|
+
on: eq(schema.employees.departmentId, schema.departments.id),
|
|
71
|
+
type: 'left'
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
where: eq(schema.employees.organisationId, ctx.securityContext.organisationId)
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
dimensions: {
|
|
78
|
+
name: {
|
|
79
|
+
name: 'name',
|
|
80
|
+
title: 'Employee Name',
|
|
81
|
+
sql: schema.employees.name,
|
|
82
|
+
type: 'string'
|
|
83
|
+
},
|
|
84
|
+
email: {
|
|
85
|
+
name: 'email',
|
|
86
|
+
title: 'Email Address',
|
|
87
|
+
sql: schema.employees.email,
|
|
88
|
+
type: 'string'
|
|
89
|
+
},
|
|
90
|
+
departmentName: {
|
|
91
|
+
name: 'departmentName',
|
|
92
|
+
title: 'Department',
|
|
93
|
+
sql: schema.departments.name,
|
|
94
|
+
type: 'string'
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
measures: {
|
|
99
|
+
count: {
|
|
100
|
+
name: 'count',
|
|
101
|
+
title: 'Total Employees',
|
|
102
|
+
sql: schema.employees.id,
|
|
103
|
+
type: 'count'
|
|
104
|
+
},
|
|
105
|
+
totalSalary: {
|
|
106
|
+
name: 'totalSalary',
|
|
107
|
+
title: 'Total Salary',
|
|
108
|
+
sql: schema.employees.salary,
|
|
109
|
+
type: 'sum',
|
|
110
|
+
format: 'currency'
|
|
111
|
+
},
|
|
112
|
+
avgSalary: {
|
|
113
|
+
name: 'avgSalary',
|
|
114
|
+
title: 'Average Salary',
|
|
115
|
+
sql: schema.employees.salary,
|
|
116
|
+
type: 'avg',
|
|
117
|
+
format: 'currency'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Setup API Server
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// server.ts
|
|
127
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
128
|
+
import { createCubeApp } from 'drizzle-cube/adapters/hono'
|
|
129
|
+
import { SemanticLayerCompiler } from 'drizzle-cube/server'
|
|
130
|
+
import postgres from 'postgres'
|
|
131
|
+
import { schema } from './schema'
|
|
132
|
+
import { employeesCube } from './cubes'
|
|
133
|
+
|
|
134
|
+
// Setup Drizzle
|
|
135
|
+
const client = postgres(process.env.DATABASE_URL!)
|
|
136
|
+
const db = drizzle(client, { schema })
|
|
137
|
+
|
|
138
|
+
// Create semantic layer
|
|
139
|
+
const semanticLayer = new SemanticLayerCompiler({
|
|
140
|
+
drizzle: db,
|
|
141
|
+
schema,
|
|
142
|
+
engineType: 'postgres'
|
|
143
|
+
})
|
|
144
|
+
semanticLayer.registerCube(employeesCube)
|
|
145
|
+
|
|
146
|
+
// Create API server with Cube.js compatibility
|
|
147
|
+
const app = createCubeApp({
|
|
148
|
+
semanticLayer,
|
|
149
|
+
drizzle: db,
|
|
150
|
+
schema,
|
|
151
|
+
getSecurityContext: async (c) => ({
|
|
152
|
+
organisationId: c.get('user')?.organisationId
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
export default app
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 5. Query from Frontend
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Use with Cube.js React SDK
|
|
163
|
+
import { useCubeQuery } from '@cubejs-client/react'
|
|
164
|
+
|
|
165
|
+
function EmployeeStats() {
|
|
166
|
+
const { resultSet, isLoading } = useCubeQuery({
|
|
167
|
+
measures: ['Employees.count', 'Employees.avgSalary'],
|
|
168
|
+
dimensions: ['Employees.departmentName'],
|
|
169
|
+
filters: [
|
|
170
|
+
{ member: 'Employees.active', operator: 'equals', values: [true] }
|
|
171
|
+
]
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
if (isLoading) return <div>Loading...</div>
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<table>
|
|
178
|
+
{resultSet.tablePivot().map((row, i) => (
|
|
179
|
+
<tr key={i}>
|
|
180
|
+
<td>{row['Employees.departmentName']}</td>
|
|
181
|
+
<td>{row['Employees.count']}</td>
|
|
182
|
+
<td>${row['Employees.avgSalary']}</td>
|
|
183
|
+
</tr>
|
|
184
|
+
))}
|
|
185
|
+
</table>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Key Features
|
|
191
|
+
|
|
192
|
+
### 🔐 Security First
|
|
193
|
+
|
|
194
|
+
All SQL is generated using Drizzle's parameterized queries, making SQL injection impossible:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// ❌ Vulnerable (string concatenation)
|
|
198
|
+
const sql = `WHERE name = '${userInput}'`
|
|
199
|
+
|
|
200
|
+
// ✅ Safe (Drizzle parameterization)
|
|
201
|
+
const condition = eq(schema.employees.name, userInput)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 🏗️ Type Safety
|
|
205
|
+
|
|
206
|
+
Get full TypeScript support from your database schema to your analytics:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const cube = defineCube('Employees', {
|
|
210
|
+
dimensions: {
|
|
211
|
+
name: {
|
|
212
|
+
name: 'name',
|
|
213
|
+
title: 'Employee Name',
|
|
214
|
+
sql: schema.employees.name, // ✅ Type-safe
|
|
215
|
+
type: 'string'
|
|
216
|
+
},
|
|
217
|
+
invalid: {
|
|
218
|
+
name: 'invalid',
|
|
219
|
+
sql: schema.employees.invalidCol, // ❌ TypeScript error
|
|
220
|
+
type: 'string'
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### ⚡ Performance
|
|
227
|
+
|
|
228
|
+
- **Prepared Statements**: Drizzle generates optimized prepared statements
|
|
229
|
+
- **Query Planning**: Database optimizes repeated queries automatically
|
|
230
|
+
- **Connection Pooling**: Leverages Drizzle's connection management
|
|
231
|
+
|
|
232
|
+
### 🧩 Framework Support
|
|
233
|
+
|
|
234
|
+
Works with multiple frameworks via adapter pattern:
|
|
235
|
+
|
|
236
|
+
- **Hono** - Built-in adapter
|
|
237
|
+
- **Express** - Coming soon
|
|
238
|
+
- **Fastify** - Coming soon
|
|
239
|
+
- **Next.js** - Coming soon
|
|
240
|
+
|
|
241
|
+
## Advanced Usage
|
|
242
|
+
|
|
243
|
+
### Complex Queries with CTEs
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { sql } from 'drizzle-orm'
|
|
247
|
+
|
|
248
|
+
const advancedCube = defineCube('DepartmentAnalytics', {
|
|
249
|
+
title: 'Department Analytics',
|
|
250
|
+
|
|
251
|
+
// For complex queries, you can use raw SQL
|
|
252
|
+
sql: (ctx) => sql`
|
|
253
|
+
WITH department_stats AS (
|
|
254
|
+
SELECT
|
|
255
|
+
d.id,
|
|
256
|
+
d.name,
|
|
257
|
+
COUNT(e.id) as employee_count,
|
|
258
|
+
AVG(e.salary) as avg_salary
|
|
259
|
+
FROM ${schema.departments} d
|
|
260
|
+
LEFT JOIN ${schema.employees} e ON d.id = e.department_id
|
|
261
|
+
WHERE d.organisation_id = ${ctx.securityContext.organisationId}
|
|
262
|
+
GROUP BY d.id, d.name
|
|
263
|
+
)
|
|
264
|
+
SELECT * FROM department_stats
|
|
265
|
+
`,
|
|
266
|
+
|
|
267
|
+
dimensions: {
|
|
268
|
+
name: {
|
|
269
|
+
name: 'name',
|
|
270
|
+
title: 'Department Name',
|
|
271
|
+
sql: sql`name`,
|
|
272
|
+
type: 'string'
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
measures: {
|
|
277
|
+
employeeCount: {
|
|
278
|
+
name: 'employeeCount',
|
|
279
|
+
title: 'Employee Count',
|
|
280
|
+
sql: sql`employee_count`,
|
|
281
|
+
type: 'number'
|
|
282
|
+
},
|
|
283
|
+
avgSalary: {
|
|
284
|
+
name: 'avgSalary',
|
|
285
|
+
title: 'Average Salary',
|
|
286
|
+
sql: sql`avg_salary`,
|
|
287
|
+
type: 'number',
|
|
288
|
+
format: 'currency'
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Advanced Security with Row-Level Security
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { and, sql } from 'drizzle-orm'
|
|
298
|
+
|
|
299
|
+
const secureCube = defineCube('SecureEmployees', {
|
|
300
|
+
title: 'Secure Employee Data',
|
|
301
|
+
|
|
302
|
+
sql: (ctx) => ({
|
|
303
|
+
from: schema.employees,
|
|
304
|
+
where: and(
|
|
305
|
+
eq(schema.employees.organisationId, ctx.securityContext.organisationId),
|
|
306
|
+
// Only show employees user has permission to see
|
|
307
|
+
ctx.securityContext.role === 'admin'
|
|
308
|
+
? sql`true`
|
|
309
|
+
: eq(schema.employees.managerId, ctx.securityContext.userId)
|
|
310
|
+
)
|
|
311
|
+
}),
|
|
312
|
+
|
|
313
|
+
dimensions: {
|
|
314
|
+
name: {
|
|
315
|
+
name: 'name',
|
|
316
|
+
title: 'Employee Name',
|
|
317
|
+
sql: schema.employees.name,
|
|
318
|
+
type: 'string'
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
measures: {
|
|
323
|
+
count: {
|
|
324
|
+
name: 'count',
|
|
325
|
+
title: 'Employee Count',
|
|
326
|
+
sql: schema.employees.id,
|
|
327
|
+
type: 'count'
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Multiple Database Support
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// PostgreSQL
|
|
337
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
338
|
+
import postgres from 'postgres'
|
|
339
|
+
|
|
340
|
+
// MySQL
|
|
341
|
+
import { drizzle } from 'drizzle-orm/mysql2'
|
|
342
|
+
import mysql from 'mysql2/promise'
|
|
343
|
+
|
|
344
|
+
// SQLite
|
|
345
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
|
346
|
+
import Database from 'better-sqlite3'
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## API Reference
|
|
350
|
+
|
|
351
|
+
### Core Functions
|
|
352
|
+
|
|
353
|
+
- `defineCube(name, definition)` - Create type-safe cube with name and configuration
|
|
354
|
+
- `SemanticLayerCompiler({ drizzle, schema, engineType })` - Setup semantic layer compiler
|
|
355
|
+
- `createCubeApp(options)` - Create Cube.js-compatible API server
|
|
356
|
+
|
|
357
|
+
### Supported Drizzle Features
|
|
358
|
+
|
|
359
|
+
- ✅ **All Database Types** - PostgreSQL, MySQL, SQLite
|
|
360
|
+
- ✅ **Query Builder** - Full Drizzle query builder support
|
|
361
|
+
- ✅ **Schema References** - Direct column references
|
|
362
|
+
- ✅ **SQL Templates** - Raw SQL with parameterization
|
|
363
|
+
- ✅ **Aggregations** - count, sum, avg, min, max, countDistinct
|
|
364
|
+
- ✅ **Joins** - Inner, left, right, full outer joins
|
|
365
|
+
- ✅ **CTEs** - Common table expressions
|
|
366
|
+
- ✅ **Subqueries** - Nested query support
|
|
367
|
+
- ✅ **Window Functions** - Advanced analytics
|
|
368
|
+
- ✅ **JSON Operations** - PostgreSQL JSON/JSONB support
|
|
369
|
+
|
|
370
|
+
### Filter Operators
|
|
371
|
+
|
|
372
|
+
Supports all Cube.js filter operators with Drizzle safety:
|
|
373
|
+
|
|
374
|
+
- `equals`, `notEquals` → `eq()`, `ne()`
|
|
375
|
+
- `contains`, `notContains` → `ilike()`, `notIlike()`
|
|
376
|
+
- `gt`, `gte`, `lt`, `lte` → `gt()`, `gte()`, `lt()`, `lte()`
|
|
377
|
+
- `set`, `notSet` → `isNotNull()`, `isNull()`
|
|
378
|
+
- `inDateRange` → `and(gte(), lte())`
|
|
379
|
+
|
|
380
|
+
## Documentation
|
|
381
|
+
|
|
382
|
+
Coming soon! 📚
|
|
383
|
+
|
|
384
|
+
## Examples
|
|
385
|
+
|
|
386
|
+
Coming soon! Check the test files for usage patterns in the meantime.
|
|
387
|
+
|
|
388
|
+
## Contributing
|
|
389
|
+
|
|
390
|
+
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md).
|
|
391
|
+
|
|
392
|
+
## Roadmap
|
|
393
|
+
|
|
394
|
+
- 🔄 **Express Adapter** - Express.js integration
|
|
395
|
+
- 🔄 **Fastify Adapter** - Fastify integration
|
|
396
|
+
- 🔄 **Next.js Adapter** - Next.js API routes
|
|
397
|
+
- 🔄 **Pre-aggregations** - Materialized view support
|
|
398
|
+
- 🔄 **Real-time Updates** - WebSocket support
|
|
399
|
+
- 🔄 **Query Caching** - Redis integration
|
|
400
|
+
|
|
401
|
+
## License
|
|
402
|
+
|
|
403
|
+
MIT © [Clifton Cunningham](https://github.com/cliftonc)
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
**Built with ❤️ for the Drizzle ORM community**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { SemanticLayerCompiler, SemanticQuery, SecurityContext, DatabaseExecutor, DrizzleDatabase } from '../../server';
|
|
3
|
+
export interface HonoAdapterOptions<TSchema extends Record<string, any> = Record<string, any>> {
|
|
4
|
+
/**
|
|
5
|
+
* The semantic layer instance to use
|
|
6
|
+
*/
|
|
7
|
+
semanticLayer: SemanticLayerCompiler<TSchema>;
|
|
8
|
+
/**
|
|
9
|
+
* Drizzle database instance (REQUIRED)
|
|
10
|
+
* This is the core of drizzle-cube - Drizzle ORM integration
|
|
11
|
+
*/
|
|
12
|
+
drizzle: DrizzleDatabase<TSchema>;
|
|
13
|
+
/**
|
|
14
|
+
* Database schema for type inference (RECOMMENDED)
|
|
15
|
+
* Provides full type safety for cube definitions
|
|
16
|
+
*/
|
|
17
|
+
schema?: TSchema;
|
|
18
|
+
/**
|
|
19
|
+
* Function to extract security context from Hono context
|
|
20
|
+
* This is where you provide your app-specific context extraction logic
|
|
21
|
+
*/
|
|
22
|
+
getSecurityContext: (c: any) => SecurityContext | Promise<SecurityContext>;
|
|
23
|
+
/**
|
|
24
|
+
* CORS configuration (optional)
|
|
25
|
+
*/
|
|
26
|
+
cors?: {
|
|
27
|
+
origin?: string | string[] | ((origin: string, c: any) => string | null | undefined);
|
|
28
|
+
allowMethods?: string[];
|
|
29
|
+
allowHeaders?: string[];
|
|
30
|
+
credentials?: boolean;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* API base path (default: '/cubejs-api/v1')
|
|
34
|
+
*/
|
|
35
|
+
basePath?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create Hono routes for Cube.js-compatible API
|
|
39
|
+
*/
|
|
40
|
+
export declare function createCubeRoutes<TSchema extends Record<string, any> = Record<string, any>>(options: HonoAdapterOptions<TSchema>): Hono<import('hono/types').BlankEnv, import('hono/types').BlankSchema, "/">;
|
|
41
|
+
/**
|
|
42
|
+
* Convenience function to create routes and mount them on an existing Hono app
|
|
43
|
+
*/
|
|
44
|
+
export declare function mountCubeRoutes<TSchema extends Record<string, any> = Record<string, any>>(app: Hono, options: HonoAdapterOptions<TSchema>): Hono<import('hono/types').BlankEnv, import('hono/types').BlankSchema, "/">;
|
|
45
|
+
/**
|
|
46
|
+
* Create a complete Hono app with Cube.js routes
|
|
47
|
+
*/
|
|
48
|
+
export declare function createCubeApp<TSchema extends Record<string, any> = Record<string, any>>(options: HonoAdapterOptions<TSchema>): Hono<import('hono/types').BlankEnv, import('hono/types').BlankSchema, "/">;
|
|
49
|
+
export type { SecurityContext, DatabaseExecutor, SemanticQuery, DrizzleDatabase };
|
|
@@ -1,5 +1,172 @@
|
|
|
1
|
-
|
|
1
|
+
import { Hono as w } from "hono";
|
|
2
|
+
var j = (f) => {
|
|
3
|
+
const u = {
|
|
4
|
+
...{
|
|
5
|
+
origin: "*",
|
|
6
|
+
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
|
|
7
|
+
allowHeaders: [],
|
|
8
|
+
exposeHeaders: []
|
|
9
|
+
},
|
|
10
|
+
...f
|
|
11
|
+
}, q = /* @__PURE__ */ ((a) => typeof a == "string" ? a === "*" ? () => a : (t) => a === t ? t : null : typeof a == "function" ? a : (t) => a.includes(t) ? t : null)(u.origin), m = ((a) => typeof a == "function" ? a : Array.isArray(a) ? () => a : () => [])(u.allowMethods);
|
|
12
|
+
return async function(t, d) {
|
|
13
|
+
var i;
|
|
14
|
+
function e(r, n) {
|
|
15
|
+
t.res.headers.set(r, n);
|
|
16
|
+
}
|
|
17
|
+
const o = q(t.req.header("origin") || "", t);
|
|
18
|
+
if (o && e("Access-Control-Allow-Origin", o), u.origin !== "*") {
|
|
19
|
+
const r = t.req.header("Vary");
|
|
20
|
+
r ? e("Vary", r) : e("Vary", "Origin");
|
|
21
|
+
}
|
|
22
|
+
if (u.credentials && e("Access-Control-Allow-Credentials", "true"), (i = u.exposeHeaders) != null && i.length && e("Access-Control-Expose-Headers", u.exposeHeaders.join(",")), t.req.method === "OPTIONS") {
|
|
23
|
+
u.maxAge != null && e("Access-Control-Max-Age", u.maxAge.toString());
|
|
24
|
+
const r = m(t.req.header("origin") || "", t);
|
|
25
|
+
r.length && e("Access-Control-Allow-Methods", r.join(","));
|
|
26
|
+
let n = u.allowHeaders;
|
|
27
|
+
if (!(n != null && n.length)) {
|
|
28
|
+
const s = t.req.header("Access-Control-Request-Headers");
|
|
29
|
+
s && (n = s.split(/\s*,\s*/));
|
|
30
|
+
}
|
|
31
|
+
return n != null && n.length && (e("Access-Control-Allow-Headers", n.join(",")), t.res.headers.append("Vary", "Access-Control-Request-Headers")), t.res.headers.delete("Content-Length"), t.res.headers.delete("Content-Type"), new Response(null, {
|
|
32
|
+
headers: t.res.headers,
|
|
33
|
+
status: 204,
|
|
34
|
+
statusText: "No Content"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
await d();
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
function C(f) {
|
|
41
|
+
const {
|
|
42
|
+
semanticLayer: l,
|
|
43
|
+
drizzle: u,
|
|
44
|
+
schema: q,
|
|
45
|
+
getSecurityContext: m,
|
|
46
|
+
cors: a,
|
|
47
|
+
basePath: t = "/cubejs-api/v1"
|
|
48
|
+
} = f, d = new w();
|
|
49
|
+
return a && d.use("/*", j(a)), l.hasExecutor() || l.setDrizzle(u, q), d.post(`${t}/load`, async (e) => {
|
|
50
|
+
var o, i;
|
|
51
|
+
try {
|
|
52
|
+
const r = await e.req.json(), n = await m(e);
|
|
53
|
+
if (!((o = r.measures) != null && o.length) && !((i = r.dimensions) != null && i.length))
|
|
54
|
+
return e.json({
|
|
55
|
+
error: "Query must specify at least one measure or dimension"
|
|
56
|
+
}, 400);
|
|
57
|
+
const s = await l.executeMultiCubeQuery(r, n);
|
|
58
|
+
return e.json({
|
|
59
|
+
data: s.data,
|
|
60
|
+
annotation: s.annotation,
|
|
61
|
+
query: r,
|
|
62
|
+
slowQuery: !1
|
|
63
|
+
});
|
|
64
|
+
} catch (r) {
|
|
65
|
+
return console.error("Query execution error:", r), e.json({
|
|
66
|
+
error: r instanceof Error ? r.message : "Query execution failed"
|
|
67
|
+
}, 500);
|
|
68
|
+
}
|
|
69
|
+
}), d.get(`${t}/load`, async (e) => {
|
|
70
|
+
var o, i;
|
|
71
|
+
try {
|
|
72
|
+
const r = e.req.query("query");
|
|
73
|
+
if (!r)
|
|
74
|
+
return e.json({
|
|
75
|
+
error: "Query parameter is required"
|
|
76
|
+
}, 400);
|
|
77
|
+
const n = JSON.parse(r), s = await m(e);
|
|
78
|
+
if (!((o = n.measures) != null && o.length) && !((i = n.dimensions) != null && i.length))
|
|
79
|
+
return e.json({
|
|
80
|
+
error: "Query must specify at least one measure or dimension"
|
|
81
|
+
}, 400);
|
|
82
|
+
const c = await l.executeMultiCubeQuery(n, s);
|
|
83
|
+
return e.json({
|
|
84
|
+
data: c.data,
|
|
85
|
+
annotation: c.annotation,
|
|
86
|
+
query: n,
|
|
87
|
+
slowQuery: !1
|
|
88
|
+
});
|
|
89
|
+
} catch (r) {
|
|
90
|
+
return console.error("Query execution error:", r), e.json({
|
|
91
|
+
error: r instanceof Error ? r.message : "Query execution failed"
|
|
92
|
+
}, 500);
|
|
93
|
+
}
|
|
94
|
+
}), d.get(`${t}/meta`, async (e) => {
|
|
95
|
+
try {
|
|
96
|
+
const o = l.getMetadata();
|
|
97
|
+
return e.json({
|
|
98
|
+
cubes: o
|
|
99
|
+
});
|
|
100
|
+
} catch (o) {
|
|
101
|
+
return console.error("Metadata error:", o), e.json({
|
|
102
|
+
error: o instanceof Error ? o.message : "Failed to fetch metadata"
|
|
103
|
+
}, 500);
|
|
104
|
+
}
|
|
105
|
+
}), d.post(`${t}/sql`, async (e) => {
|
|
106
|
+
var o, i, r, n;
|
|
107
|
+
try {
|
|
108
|
+
const s = await e.req.json(), c = await m(e);
|
|
109
|
+
if (!((o = s.measures) != null && o.length) && !((i = s.dimensions) != null && i.length))
|
|
110
|
+
return e.json({
|
|
111
|
+
error: "Query must specify at least one measure or dimension"
|
|
112
|
+
}, 400);
|
|
113
|
+
const y = ((r = s.measures) == null ? void 0 : r[0]) || ((n = s.dimensions) == null ? void 0 : n[0]);
|
|
114
|
+
if (!y)
|
|
115
|
+
return e.json({
|
|
116
|
+
error: "No measures or dimensions specified"
|
|
117
|
+
}, 400);
|
|
118
|
+
const g = y.split(".")[0], h = await l.generateSQL(g, s, c);
|
|
119
|
+
return e.json({
|
|
120
|
+
sql: h.sql,
|
|
121
|
+
params: h.params || [],
|
|
122
|
+
query: s
|
|
123
|
+
});
|
|
124
|
+
} catch (s) {
|
|
125
|
+
return console.error("SQL generation error:", s), e.json({
|
|
126
|
+
error: s instanceof Error ? s.message : "SQL generation failed"
|
|
127
|
+
}, 500);
|
|
128
|
+
}
|
|
129
|
+
}), d.get(`${t}/sql`, async (e) => {
|
|
130
|
+
var o, i, r, n;
|
|
131
|
+
try {
|
|
132
|
+
const s = e.req.query("query");
|
|
133
|
+
if (!s)
|
|
134
|
+
return e.json({
|
|
135
|
+
error: "Query parameter is required"
|
|
136
|
+
}, 400);
|
|
137
|
+
const c = JSON.parse(s), y = await m(e);
|
|
138
|
+
if (!((o = c.measures) != null && o.length) && !((i = c.dimensions) != null && i.length))
|
|
139
|
+
return e.json({
|
|
140
|
+
error: "Query must specify at least one measure or dimension"
|
|
141
|
+
}, 400);
|
|
142
|
+
const g = ((r = c.measures) == null ? void 0 : r[0]) || ((n = c.dimensions) == null ? void 0 : n[0]);
|
|
143
|
+
if (!g)
|
|
144
|
+
return e.json({
|
|
145
|
+
error: "No measures or dimensions specified"
|
|
146
|
+
}, 400);
|
|
147
|
+
const h = g.split(".")[0], p = await l.generateSQL(h, c, y);
|
|
148
|
+
return e.json({
|
|
149
|
+
sql: p.sql,
|
|
150
|
+
params: p.params || [],
|
|
151
|
+
query: c
|
|
152
|
+
});
|
|
153
|
+
} catch (s) {
|
|
154
|
+
return console.error("SQL generation error:", s), e.json({
|
|
155
|
+
error: s instanceof Error ? s.message : "SQL generation failed"
|
|
156
|
+
}, 500);
|
|
157
|
+
}
|
|
158
|
+
}), d;
|
|
159
|
+
}
|
|
160
|
+
function x(f, l) {
|
|
161
|
+
const u = C(l);
|
|
162
|
+
return f.route("/", u), f;
|
|
163
|
+
}
|
|
164
|
+
function A(f) {
|
|
165
|
+
const l = new w();
|
|
166
|
+
return x(l, f);
|
|
2
167
|
}
|
|
3
168
|
export {
|
|
4
|
-
|
|
169
|
+
A as createCubeApp,
|
|
170
|
+
C as createCubeRoutes,
|
|
171
|
+
x as mountCubeRoutes
|
|
5
172
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SemanticLayerCompiler, SecurityContext, DatabaseExecutor } from '../server';
|
|
2
|
+
/**
|
|
3
|
+
* Base adapter configuration
|
|
4
|
+
*/
|
|
5
|
+
export interface BaseAdapterOptions {
|
|
6
|
+
semanticLayer: SemanticLayerCompiler;
|
|
7
|
+
databaseExecutor?: DatabaseExecutor;
|
|
8
|
+
basePath?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Framework-specific context extractor
|
|
12
|
+
* Each framework adapter will provide their own context type
|
|
13
|
+
*/
|
|
14
|
+
export interface ContextExtractor<TContext = any> {
|
|
15
|
+
(context: TContext): SecurityContext | Promise<SecurityContext>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Standard CORS configuration
|
|
19
|
+
*/
|
|
20
|
+
export interface CorsConfig {
|
|
21
|
+
origin?: string | string[] | ((origin: string) => boolean);
|
|
22
|
+
allowMethods?: string[];
|
|
23
|
+
allowHeaders?: string[];
|
|
24
|
+
credentials?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Standard adapter response format
|
|
28
|
+
*/
|
|
29
|
+
export interface AdapterResponse {
|
|
30
|
+
data?: any;
|
|
31
|
+
error?: string;
|
|
32
|
+
status?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Future adapter interface (for Express, Fastify, etc.)
|
|
36
|
+
*/
|
|
37
|
+
export interface AdapterFactory<TOptions extends BaseAdapterOptions, TApp = any> {
|
|
38
|
+
createRoutes(options: TOptions): TApp;
|
|
39
|
+
mountRoutes?(app: TApp, options: TOptions): TApp;
|
|
40
|
+
createApp?(options: TOptions): TApp;
|
|
41
|
+
}
|