Skip to content

Commit a36384d

Browse files
authored
Merge pull request #26 from nikosgram/main
feat(adapter-sveltekit): add hook-based SvelteKit integration and docs
2 parents 85bdbc8 + fca11e1 commit a36384d

27 files changed

+2473
-4
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ That's it! 🎉 No column definitions needed.
8484
| 🔐 **Role-based Visibility** | Control column visibility by user role |
8585
| 🗑️ **Soft Delete Support** | Built-in soft delete filtering |
8686
87-
**Plus:** Caching plugin, multiple backend adapters (Hono, Express, Next.js, Elysia), computed columns, relationships & joins, type generation, OpenAPI spec, and more...
87+
**Plus:** Caching plugin, multiple backend adapters (Hono, Express, Next.js, SvelteKit, Elysia), computed columns, relationships & joins, type generation, OpenAPI spec, and more...
8888
8989
📚 **[Explore all features in the docs ](https://jacksonkasi.gitbook.io/tablecraft)**
9090
@@ -100,6 +100,7 @@ That's it! 🎉 No column definitions needed.
100100
| `@tablecraft/client` | Client utilities for API communication |
101101
| `@tablecraft/adapter-hono` | Hono server adapter |
102102
| `@tablecraft/adapter-next` | Next.js server adapter |
103+
| `@tablecraft/adapter-sveltekit` | SvelteKit server adapter |
103104
| `@tablecraft/adapter-express` | Express server adapter |
104105
| `@tablecraft/adapter-elysia` | Elysia server adapter |
105106
| `@tablecraft/plugin-cache` | Caching plugin |

apps/sveltekit-example/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
.svelte-kit
3+
build
4+
.env
5+
.env.*
6+
!.env.example
7+
vite.config.js.timestamp-*
8+
vite.config.ts.timestamp-*
9+
drizzle/
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineConfig } from 'drizzle-kit';
2+
import 'dotenv/config';
3+
4+
const dbUrl = process.env.DATABASE_URL;
5+
if (!dbUrl) {
6+
throw new Error('DATABASE_URL environment variable is missing');
7+
}
8+
9+
export default defineConfig({
10+
out: './drizzle',
11+
schema: './src/lib/server/db/schema.ts',
12+
dialect: 'postgresql',
13+
dbCredentials: {
14+
url: dbUrl,
15+
},
16+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "sveltekit-example",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite dev",
8+
"build": "vite build",
9+
"preview": "vite preview",
10+
"check": "svelte-check --tsconfig ./tsconfig.json",
11+
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
12+
"db:generate": "drizzle-kit generate",
13+
"db:push": "drizzle-kit push",
14+
"seed": "bun run src/lib/server/db/seed.ts",
15+
"test": "vitest run"
16+
},
17+
"devDependencies": {
18+
"@sveltejs/adapter-auto": "^7.0.1",
19+
"@sveltejs/kit": "^2.16.0",
20+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
21+
"svelte": "^5.0.0",
22+
"svelte-check": "^4.1.4",
23+
"typescript": "^5.0.0",
24+
"vite": "^6.0.0",
25+
"drizzle-kit": "^0.30.4",
26+
"vitest": "^2.1.8"
27+
},
28+
"dependencies": {
29+
"@tablecraft/engine": "workspace:*",
30+
"@tablecraft/adapter-sveltekit": "workspace:*",
31+
"drizzle-orm": "^0.45.1",
32+
"postgres": "^3.4.5",
33+
"dotenv": "^16.4.7"
34+
}
35+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// See https://kit.svelte.dev/docs/types#app
2+
// for information about these interfaces
3+
declare global {
4+
namespace App {
5+
// interface Error {}
6+
interface Locals {
7+
user?: {
8+
id: string;
9+
roles: string[];
10+
};
11+
tenantId?: string | number;
12+
}
13+
// interface PageData {}
14+
// interface PageState {}
15+
// interface Platform {}
16+
}
17+
}
18+
19+
export {};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
%sveltekit.head%
8+
</head>
9+
<body data-sveltekit-preload-data="hover">
10+
<div style="display: contents">%sveltekit.body%</div>
11+
</body>
12+
</html>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Handle } from '@sveltejs/kit';
2+
import { sequence } from '@sveltejs/kit/hooks';
3+
import { createSvelteKitHandle } from '@tablecraft/adapter-sveltekit';
4+
import { db } from '$lib/server/db';
5+
import * as schema from '$lib/server/db/schema';
6+
import { configs } from '$lib/server/tablecraft.config';
7+
8+
const authHandle: Handle = async ({ event, resolve }) => {
9+
// Simulate basic authentication
10+
// In a real app, you would verify a session token here
11+
const authHeader = event.request.headers.get('Authorization');
12+
13+
if (authHeader === 'Bearer admin-token') {
14+
event.locals.user = { id: '1', roles: ['admin'] };
15+
delete event.locals.tenantId; // Admin sees all
16+
} else if (authHeader === 'Bearer member-token') {
17+
event.locals.user = { id: '2', roles: ['member'] };
18+
event.locals.tenantId = 1; // Locked to tenant 1
19+
} else {
20+
delete event.locals.user;
21+
delete event.locals.tenantId;
22+
}
23+
24+
return resolve(event);
25+
};
26+
27+
const tablecraftHandle = createSvelteKitHandle({
28+
db,
29+
schema,
30+
configs,
31+
prefix: '/api/data',
32+
enableDiscovery: true,
33+
getContext: async (event) => ({
34+
user: event.locals.user,
35+
tenantId: event.locals.tenantId
36+
})
37+
});
38+
39+
export const handle: Handle = sequence(authHandle, tablecraftHandle);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { drizzle } from 'drizzle-orm/postgres-js';
2+
import postgres from 'postgres';
3+
import * as schema from './schema';
4+
import 'dotenv/config';
5+
6+
const dbUrl = process.env.DATABASE_URL;
7+
if (!dbUrl) {
8+
throw new Error('DATABASE_URL environment variable is missing');
9+
}
10+
11+
const client = postgres(dbUrl);
12+
export const db = drizzle(client, { schema });
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { pgTable, serial, text, integer, timestamp, boolean, jsonb, decimal, uuid } from 'drizzle-orm/pg-core';
2+
import { relations } from 'drizzle-orm';
3+
4+
// 1. Tenants (SaaS context)
5+
export const tenants = pgTable('tenants', {
6+
id: serial('id').primaryKey(),
7+
name: text('name').notNull(),
8+
slug: text('slug').unique().notNull(),
9+
plan: text('plan').default('free'), // free, pro, enterprise
10+
createdAt: timestamp('created_at').defaultNow(),
11+
});
12+
13+
// 2. Users (Belong to tenants)
14+
export const users = pgTable('users', {
15+
id: serial('id').primaryKey(),
16+
tenantId: integer('tenant_id').references(() => tenants.id).notNull(),
17+
email: text('email').notNull(),
18+
role: text('role').default('member'), // admin, member, viewer
19+
isActive: boolean('is_active').default(true),
20+
createdAt: timestamp('created_at').defaultNow(),
21+
});
22+
23+
// 3. Products (Catalog)
24+
export const products = pgTable('products', {
25+
id: serial('id').primaryKey(),
26+
tenantId: integer('tenant_id').references(() => tenants.id).notNull(),
27+
name: text('name').notNull(),
28+
description: text('description'),
29+
price: decimal('price', { precision: 10, scale: 2 }).notNull(),
30+
category: text('category').notNull(),
31+
tags: jsonb('tags').$type<string[]>(), // Array of strings
32+
metadata: jsonb('metadata'), // Flexible JSON
33+
isArchived: boolean('is_archived').default(false),
34+
});
35+
36+
// 4. Orders (Transactional)
37+
export const orders = pgTable('orders', {
38+
id: serial('id').primaryKey(),
39+
tenantId: integer('tenant_id').references(() => tenants.id).notNull(),
40+
userId: integer('user_id').references(() => users.id).notNull(),
41+
status: text('status').default('pending'), // pending, paid, shipped, cancelled
42+
total: decimal('total', { precision: 10, scale: 2 }).default('0'),
43+
createdAt: timestamp('created_at').defaultNow(),
44+
deletedAt: timestamp('deleted_at'), // Soft delete
45+
});
46+
47+
// 5. Order Items (M:N relation details)
48+
export const orderItems = pgTable('order_items', {
49+
id: serial('id').primaryKey(),
50+
orderId: integer('order_id').references(() => orders.id).notNull(),
51+
productId: integer('product_id').references(() => products.id).notNull(),
52+
quantity: integer('quantity').default(1),
53+
unitPrice: decimal('unit_price', { precision: 10, scale: 2 }).notNull(),
54+
});
55+
56+
// --- Relations ---
57+
58+
export const tenantRelations = relations(tenants, ({ many }) => ({
59+
users: many(users),
60+
products: many(products),
61+
orders: many(orders),
62+
}));
63+
64+
export const userRelations = relations(users, ({ one, many }) => ({
65+
tenant: one(tenants, {
66+
fields: [users.tenantId],
67+
references: [tenants.id],
68+
}),
69+
orders: many(orders),
70+
}));
71+
72+
export const productRelations = relations(products, ({ one }) => ({
73+
tenant: one(tenants, {
74+
fields: [products.tenantId],
75+
references: [tenants.id],
76+
}),
77+
}));
78+
79+
export const orderRelations = relations(orders, ({ one, many }) => ({
80+
tenant: one(tenants, {
81+
fields: [orders.tenantId],
82+
references: [tenants.id],
83+
}),
84+
user: one(users, {
85+
fields: [orders.userId],
86+
references: [users.id],
87+
}),
88+
items: many(orderItems),
89+
}));
90+
91+
export const orderItemRelations = relations(orderItems, ({ one }) => ({
92+
order: one(orders, {
93+
fields: [orderItems.orderId],
94+
references: [orders.id],
95+
}),
96+
product: one(products, {
97+
fields: [orderItems.productId],
98+
references: [products.id],
99+
}),
100+
}));
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { db } from './index';
2+
import { tenants, users, products, orders, orderItems } from './schema';
3+
import { sql } from 'drizzle-orm';
4+
5+
async function seed() {
6+
console.log('🌱 Seeding database...');
7+
8+
// Cleanup
9+
console.log('Cleaning up old data...');
10+
await db.delete(orderItems);
11+
await db.delete(orders);
12+
await db.delete(products);
13+
await db.delete(users);
14+
await db.delete(tenants);
15+
16+
// 1. Tenants
17+
console.log('Creating tenants...');
18+
const tenantData = [
19+
{ name: 'Acme Corp', slug: 'acme', plan: 'enterprise' },
20+
{ name: 'StartUp Inc', slug: 'startup', plan: 'pro' },
21+
{ name: 'Dev Studio', slug: 'devstudio', plan: 'free' },
22+
];
23+
24+
const createdTenants = await db.insert(tenants).values(tenantData).returning();
25+
26+
// 2. Users & Products per Tenant
27+
for (const tenant of createdTenants) {
28+
console.log(`Populating tenant: ${tenant.name}`);
29+
30+
// Users
31+
const usersData = Array.from({ length: 10 }).map((_, i) => ({
32+
tenantId: tenant.id,
33+
email: `${tenant.slug}-user${i + 1}@example.com`,
34+
role: i === 0 ? 'admin' : i % 5 === 0 ? 'viewer' : 'member',
35+
isActive: true,
36+
}));
37+
const createdUsers = await db.insert(users).values(usersData).returning();
38+
39+
// Products
40+
const productsData = Array.from({ length: 20 }).map((_, i) => ({
41+
tenantId: tenant.id,
42+
name: `${tenant.name} Product ${i + 1}`,
43+
description: `Description for product ${i + 1}`,
44+
price: (Math.random() * 100 + 10).toFixed(2),
45+
category: i % 3 === 0 ? 'electronics' : i % 3 === 1 ? 'clothing' : 'home',
46+
tags: ['new', 'sale'],
47+
isArchived: Math.random() > 0.8,
48+
}));
49+
const createdProducts = await db.insert(products).values(productsData).returning();
50+
51+
// Orders
52+
const ordersData = [];
53+
for (let i = 0; i < 50; i++) {
54+
const user = createdUsers[Math.floor(Math.random() * createdUsers.length)];
55+
ordersData.push({
56+
tenantId: tenant.id,
57+
userId: user.id,
58+
status: ['pending', 'paid', 'shipped', 'cancelled'][Math.floor(Math.random() * 4)],
59+
total: '0', // Will update after items
60+
createdAt: new Date(Date.now() - Math.floor(Math.random() * 10000000000)),
61+
});
62+
}
63+
const createdOrders = await db.insert(orders).values(ordersData).returning();
64+
65+
// Order Items
66+
const orderItemsData = [];
67+
for (const order of createdOrders) {
68+
let orderTotal = 0;
69+
const numItems = Math.floor(Math.random() * 5) + 1;
70+
71+
for (let k = 0; k < numItems; k++) {
72+
const product = createdProducts[Math.floor(Math.random() * createdProducts.length)];
73+
const qty = Math.floor(Math.random() * 3) + 1;
74+
const price = parseFloat(product.price as string);
75+
76+
orderItemsData.push({
77+
orderId: order.id,
78+
productId: product.id,
79+
quantity: qty,
80+
unitPrice: product.price,
81+
});
82+
83+
orderTotal += price * qty;
84+
}
85+
86+
// Update order total
87+
await db.update(orders)
88+
.set({ total: orderTotal.toFixed(2) })
89+
.where(sql`${orders.id} = ${order.id}`);
90+
}
91+
92+
await db.insert(orderItems).values(orderItemsData);
93+
}
94+
95+
console.log('✅ Seed complete!');
96+
process.exit(0);
97+
}
98+
99+
seed().catch((err) => {
100+
console.error('Seed failed:', err);
101+
process.exit(1);
102+
});

0 commit comments

Comments
 (0)