drizzle-cube 0.1.0 → 0.1.1

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 ADDED
@@ -0,0 +1,322 @@
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
+ [![NPM Version](https://img.shields.io/npm/v/drizzle-cube)](https://www.npmjs.com/package/drizzle-cube)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)](https://www.typescriptlang.org/)
9
+ [![Drizzle ORM](https://img.shields.io/badge/Drizzle%20ORM-0.33+-green)](https://orm.drizzle.team/)
10
+ [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](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, eq } from 'drizzle-cube/server'
58
+ import { schema } from './schema'
59
+
60
+ export const employeesCube = defineCube(schema, {
61
+ name: 'Employees',
62
+
63
+ // Use Drizzle query builder for type safety
64
+ sql: ({ db, securityContext }) =>
65
+ db.select()
66
+ .from(schema.employees)
67
+ .leftJoin(schema.departments, eq(schema.employees.departmentId, schema.departments.id))
68
+ .where(eq(schema.employees.organisationId, securityContext.organisationId)),
69
+
70
+ dimensions: {
71
+ name: { sql: schema.employees.name, type: 'string' },
72
+ email: { sql: schema.employees.email, type: 'string' },
73
+ departmentName: { sql: schema.departments.name, type: 'string' }
74
+ },
75
+
76
+ measures: {
77
+ count: { sql: schema.employees.id, type: 'count' },
78
+ totalSalary: { sql: schema.employees.salary, type: 'sum', format: 'currency' },
79
+ avgSalary: { sql: schema.employees.salary, type: 'avg', format: 'currency' }
80
+ }
81
+ })
82
+ ```
83
+
84
+ ### 4. Setup API Server
85
+
86
+ ```typescript
87
+ // server.ts
88
+ import { drizzle } from 'drizzle-orm/postgres-js'
89
+ import { createCubeApp } from 'drizzle-cube/adapters/hono'
90
+ import { SemanticLayerCompiler } from 'drizzle-cube/server'
91
+ import postgres from 'postgres'
92
+
93
+ // Setup Drizzle
94
+ const client = postgres(process.env.DATABASE_URL!)
95
+ const db = drizzle(client, { schema })
96
+
97
+ // Create semantic layer
98
+ const semanticLayer = new SemanticLayerCompiler({ drizzle: db, schema })
99
+ semanticLayer.registerCube(employeesCube)
100
+
101
+ // Create API server with Cube.js compatibility
102
+ const app = createCubeApp({
103
+ semanticLayer,
104
+ drizzle: db,
105
+ schema,
106
+ getSecurityContext: async (c) => ({
107
+ organisationId: c.get('user')?.organisationId
108
+ })
109
+ })
110
+
111
+ export default app
112
+ ```
113
+
114
+ ### 5. Query from Frontend
115
+
116
+ ```typescript
117
+ // Use with Cube.js React SDK
118
+ import { useCubeQuery } from '@cubejs-client/react'
119
+
120
+ function EmployeeStats() {
121
+ const { resultSet, isLoading } = useCubeQuery({
122
+ measures: ['Employees.count', 'Employees.avgSalary'],
123
+ dimensions: ['Employees.departmentName'],
124
+ filters: [
125
+ { member: 'Employees.active', operator: 'equals', values: [true] }
126
+ ]
127
+ })
128
+
129
+ if (isLoading) return <div>Loading...</div>
130
+
131
+ return (
132
+ <table>
133
+ {resultSet.tablePivot().map((row, i) => (
134
+ <tr key={i}>
135
+ <td>{row['Employees.departmentName']}</td>
136
+ <td>{row['Employees.count']}</td>
137
+ <td>${row['Employees.avgSalary']}</td>
138
+ </tr>
139
+ ))}
140
+ </table>
141
+ )
142
+ }
143
+ ```
144
+
145
+ ## Key Features
146
+
147
+ ### 🔐 Security First
148
+
149
+ All SQL is generated using Drizzle's parameterized queries, making SQL injection impossible:
150
+
151
+ ```typescript
152
+ // ❌ Vulnerable (string concatenation)
153
+ const sql = `WHERE name = '${userInput}'`
154
+
155
+ // ✅ Safe (Drizzle parameterization)
156
+ const condition = eq(schema.employees.name, userInput)
157
+ ```
158
+
159
+ ### 🏗️ Type Safety
160
+
161
+ Get full TypeScript support from your database schema to your analytics:
162
+
163
+ ```typescript
164
+ const cube = defineCube(schema, {
165
+ dimensions: {
166
+ name: { sql: schema.employees.name }, // ✅ Type-safe
167
+ invalid: { sql: schema.employees.invalidCol } // ❌ TypeScript error
168
+ }
169
+ })
170
+ ```
171
+
172
+ ### ⚡ Performance
173
+
174
+ - **Prepared Statements**: Drizzle generates optimized prepared statements
175
+ - **Query Planning**: Database optimizes repeated queries automatically
176
+ - **Connection Pooling**: Leverages Drizzle's connection management
177
+
178
+ ### 🧩 Framework Support
179
+
180
+ Works with multiple frameworks via adapter pattern:
181
+
182
+ - **Hono** - Built-in adapter
183
+ - **Express** - Coming soon
184
+ - **Fastify** - Coming soon
185
+ - **Next.js** - Coming soon
186
+
187
+ ## Advanced Usage
188
+
189
+ ### Complex Queries with CTEs
190
+
191
+ ```typescript
192
+ const advancedCube = defineCube(schema, {
193
+ name: 'DepartmentAnalytics',
194
+
195
+ sql: ({ db, securityContext }) => sql`
196
+ WITH department_stats AS (
197
+ SELECT
198
+ d.id,
199
+ d.name,
200
+ COUNT(e.id) as employee_count,
201
+ AVG(e.salary) as avg_salary
202
+ FROM ${schema.departments} d
203
+ LEFT JOIN ${schema.employees} e ON d.id = e.department_id
204
+ WHERE d.organisation_id = ${securityContext.organisationId}
205
+ GROUP BY d.id, d.name
206
+ )
207
+ SELECT * FROM department_stats
208
+ `,
209
+
210
+ dimensions: {
211
+ name: { sql: sql`name`, type: 'string' }
212
+ },
213
+
214
+ measures: {
215
+ employeeCount: { sql: sql`employee_count`, type: 'number' },
216
+ avgSalary: { sql: sql`avg_salary`, type: 'number', format: 'currency' }
217
+ }
218
+ })
219
+ ```
220
+
221
+ ### Advanced Security with Row-Level Security
222
+
223
+ ```typescript
224
+ const secureCube = defineCube(schema, {
225
+ name: 'SecureEmployees',
226
+
227
+ sql: ({ db, securityContext }) =>
228
+ db.select()
229
+ .from(schema.employees)
230
+ .where(
231
+ and(
232
+ eq(schema.employees.organisationId, securityContext.organisationId),
233
+ // Only show employees user has permission to see
234
+ securityContext.role === 'admin'
235
+ ? sql`true`
236
+ : eq(schema.employees.managerId, securityContext.userId)
237
+ )
238
+ )
239
+ })
240
+ ```
241
+
242
+ ### Multiple Database Support
243
+
244
+ ```typescript
245
+ // PostgreSQL
246
+ import { drizzle } from 'drizzle-orm/postgres-js'
247
+ import postgres from 'postgres'
248
+
249
+ // MySQL
250
+ import { drizzle } from 'drizzle-orm/mysql2'
251
+ import mysql from 'mysql2/promise'
252
+
253
+ // SQLite
254
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
255
+ import Database from 'better-sqlite3'
256
+ ```
257
+
258
+ ## API Reference
259
+
260
+ ### Core Functions
261
+
262
+ - `defineCube(schema, definition)` - Create type-safe cube
263
+ - `createSemanticLayer({ drizzle, schema })` - Setup semantic layer
264
+ - `createCubeApp(options)` - Create Cube.js API server
265
+
266
+ ### Supported Drizzle Features
267
+
268
+ - ✅ **All Database Types** - PostgreSQL, MySQL, SQLite
269
+ - ✅ **Query Builder** - Full Drizzle query builder support
270
+ - ✅ **Schema References** - Direct column references
271
+ - ✅ **SQL Templates** - Raw SQL with parameterization
272
+ - ✅ **Aggregations** - count, sum, avg, min, max, countDistinct
273
+ - ✅ **Joins** - Inner, left, right, full outer joins
274
+ - ✅ **CTEs** - Common table expressions
275
+ - ✅ **Subqueries** - Nested query support
276
+ - ✅ **Window Functions** - Advanced analytics
277
+ - ✅ **JSON Operations** - PostgreSQL JSON/JSONB support
278
+
279
+ ### Filter Operators
280
+
281
+ Supports all Cube.js filter operators with Drizzle safety:
282
+
283
+ - `equals`, `notEquals` → `eq()`, `ne()`
284
+ - `contains`, `notContains` → `ilike()`, `notIlike()`
285
+ - `gt`, `gte`, `lt`, `lte` → `gt()`, `gte()`, `lt()`, `lte()`
286
+ - `set`, `notSet` → `isNotNull()`, `isNull()`
287
+ - `inDateRange` → `and(gte(), lte())`
288
+
289
+ ## Documentation
290
+
291
+ 📚 **[Complete Documentation](https://drizzle-cube.dev)**
292
+ 🏗️ **[API Reference](https://drizzle-cube.dev/api)**
293
+ 🎯 **[Drizzle Integration Guide](./docs/drizzle-integration.md)**
294
+ 🚀 **[Migration Guide](https://drizzle-cube.dev/migration)**
295
+
296
+ ## Examples
297
+
298
+ - **[Basic Hono App](./examples/hono-basic/)**
299
+ - **[Advanced Security](./examples/hono-security/)**
300
+ - **[Multi-tenant SaaS](./examples/multi-tenant/)**
301
+ - **[Real-time Dashboard](./examples/dashboard/)**
302
+
303
+ ## Contributing
304
+
305
+ We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md).
306
+
307
+ ## Roadmap
308
+
309
+ - 🔄 **Express Adapter** - Express.js integration
310
+ - 🔄 **Fastify Adapter** - Fastify integration
311
+ - 🔄 **Next.js Adapter** - Next.js API routes
312
+ - 🔄 **Pre-aggregations** - Materialized view support
313
+ - 🔄 **Real-time Updates** - WebSocket support
314
+ - 🔄 **Query Caching** - Redis integration
315
+
316
+ ## License
317
+
318
+ MIT © [Clifton Cunningham](https://github.com/cliftonc)
319
+
320
+ ---
321
+
322
+ **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
- function o() {
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
- o as createHonoRoutes
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
+ }