index.ts242 lines · main
1import type { TemplateDef } from './types.js';
2
3export type { TemplateDef, TemplateTable } from './types.js';
4
5/**
6 * Starter templates. The flagship is `contacts-crm`; the other three share
7 * the same engine and are pure data recipes on top of it.
8 *
9 * Conventions:
10 * - Every table has a `uuid` primary key defaulting to gen_random_uuid().
11 * - Tables are listed in FK order (parents before children).
12 * - Referenced sample rows carry explicit literal uuids so child rows can
13 * point at them.
14 */
15
16const ID = (n: string) => `a0000000-0000-4000-8000-0000000000${n}`;
17
18const contactsCrm: TemplateDef = {
19 id: 'contacts-crm',
20 name: 'Contacts / CRM',
21 description: 'Track people, companies and deals. A simple customer relationship manager.',
22 icon: '👥',
23 tables: [
24 {
25 tableName: 'companies',
26 columns: [
27 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
28 { name: 'name', type: 'text', notNull: true },
29 { name: 'website', type: 'text' },
30 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
31 ],
32 rows: [
33 { id: ID('a1'), name: 'Acme NV', website: 'https://acme.example' },
34 { id: ID('a2'), name: 'Globex BV', website: 'https://globex.example' },
35 ],
36 },
37 {
38 tableName: 'contacts',
39 columns: [
40 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
41 { name: 'first_name', type: 'text', notNull: true },
42 { name: 'last_name', type: 'text' },
43 { name: 'email', type: 'text' },
44 { name: 'phone', type: 'text' },
45 {
46 name: 'company_id',
47 type: 'uuid',
48 references: { table: 'companies', column: 'id', onDelete: 'setNull' },
49 },
50 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
51 ],
52 rows: [
53 { id: ID('b1'), first_name: 'Anke', last_name: 'Peeters', email: 'anke@acme.example', phone: '+32 470 00 00 01', company_id: ID('a1') },
54 { id: ID('b2'), first_name: 'Tom', last_name: 'Janssens', email: 'tom@globex.example', company_id: ID('a2') },
55 { id: ID('b3'), first_name: 'Lina', last_name: 'De Smet', email: 'lina@acme.example', company_id: ID('a1') },
56 ],
57 },
58 {
59 tableName: 'deals',
60 columns: [
61 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
62 { name: 'title', type: 'text', notNull: true },
63 { name: 'amount', type: 'numeric' },
64 { name: 'stage', type: 'text', notNull: true, defaultExpr: "'lead'" },
65 {
66 name: 'contact_id',
67 type: 'uuid',
68 references: { table: 'contacts', column: 'id', onDelete: 'setNull' },
69 },
70 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
71 ],
72 rows: [
73 { id: ID('c1'), title: 'Acme website rebuild', amount: 4500, stage: 'proposal', contact_id: ID('b1') },
74 { id: ID('c2'), title: 'Globex support retainer', amount: 1200, stage: 'lead', contact_id: ID('b2') },
75 ],
76 },
77 ],
78};
79
80const inventory: TemplateDef = {
81 id: 'inventory',
82 name: 'Inventory / stock',
83 description: 'Track products, stock levels and suppliers.',
84 icon: '📦',
85 tables: [
86 {
87 tableName: 'suppliers',
88 columns: [
89 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
90 { name: 'name', type: 'text', notNull: true },
91 { name: 'email', type: 'text' },
92 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
93 ],
94 rows: [
95 { id: ID('a1'), name: 'North Supply Co', email: 'sales@northsupply.example' },
96 { id: ID('a2'), name: 'Benelux Parts', email: 'orders@beneluxparts.example' },
97 ],
98 },
99 {
100 tableName: 'products',
101 columns: [
102 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
103 { name: 'name', type: 'text', notNull: true },
104 { name: 'sku', type: 'text' },
105 { name: 'quantity', type: 'integer', notNull: true, defaultExpr: '0' },
106 { name: 'price', type: 'numeric' },
107 {
108 name: 'supplier_id',
109 type: 'uuid',
110 references: { table: 'suppliers', column: 'id', onDelete: 'setNull' },
111 },
112 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
113 ],
114 rows: [
115 { id: ID('b1'), name: 'Widget A', sku: 'WID-A', quantity: 120, price: 9.99, supplier_id: ID('a1') },
116 { id: ID('b2'), name: 'Widget B', sku: 'WID-B', quantity: 40, price: 14.5, supplier_id: ID('a1') },
117 { id: ID('b3'), name: 'Cable 2m', sku: 'CBL-2M', quantity: 300, price: 3.25, supplier_id: ID('a2') },
118 ],
119 },
120 ],
121};
122
123const bookings: TemplateDef = {
124 id: 'bookings',
125 name: 'Bookings / appointments',
126 description: 'Track clients, services and appointments.',
127 icon: '📅',
128 tables: [
129 {
130 tableName: 'clients',
131 columns: [
132 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
133 { name: 'name', type: 'text', notNull: true },
134 { name: 'email', type: 'text' },
135 { name: 'phone', type: 'text' },
136 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
137 ],
138 rows: [
139 { id: ID('a1'), name: 'Marie Claes', email: 'marie@example.com', phone: '+32 471 11 11 11' },
140 { id: ID('a2'), name: 'Joris Vermeulen', email: 'joris@example.com' },
141 ],
142 },
143 {
144 tableName: 'services',
145 columns: [
146 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
147 { name: 'name', type: 'text', notNull: true },
148 { name: 'duration_min', type: 'integer', notNull: true, defaultExpr: '30' },
149 { name: 'price', type: 'numeric' },
150 ],
151 rows: [
152 { id: ID('b1'), name: 'Consultation', duration_min: 30, price: 40 },
153 { id: ID('b2'), name: 'Full session', duration_min: 60, price: 75 },
154 ],
155 },
156 {
157 tableName: 'appointments',
158 columns: [
159 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
160 {
161 name: 'client_id',
162 type: 'uuid',
163 references: { table: 'clients', column: 'id', onDelete: 'cascade' },
164 },
165 {
166 name: 'service_id',
167 type: 'uuid',
168 references: { table: 'services', column: 'id', onDelete: 'setNull' },
169 },
170 { name: 'starts_at', type: 'timestamptz' },
171 { name: 'notes', type: 'text' },
172 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
173 ],
174 rows: [
175 { id: ID('c1'), client_id: ID('a1'), service_id: ID('b1'), notes: 'First visit' },
176 { id: ID('c2'), client_id: ID('a2'), service_id: ID('b2'), notes: 'Follow-up' },
177 ],
178 },
179 ],
180};
181
182const tasks: TemplateDef = {
183 id: 'tasks',
184 name: 'Project / task tracker',
185 description: 'Track projects, tasks, status and due dates.',
186 icon: '✅',
187 tables: [
188 {
189 tableName: 'task_projects',
190 columns: [
191 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
192 { name: 'name', type: 'text', notNull: true },
193 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
194 ],
195 rows: [
196 { id: ID('a1'), name: 'Website launch' },
197 { id: ID('a2'), name: 'Office move' },
198 ],
199 },
200 {
201 tableName: 'tasks',
202 columns: [
203 { name: 'id', type: 'uuid', primaryKey: true, defaultExpr: 'gen_random_uuid()' },
204 { name: 'title', type: 'text', notNull: true },
205 { name: 'status', type: 'text', notNull: true, defaultExpr: "'todo'" },
206 { name: 'due_date', type: 'timestamptz' },
207 {
208 name: 'project_id',
209 type: 'uuid',
210 references: { table: 'task_projects', column: 'id', onDelete: 'cascade' },
211 },
212 { name: 'created_at', type: 'timestamptz', notNull: true, defaultExpr: 'now()' },
213 ],
214 rows: [
215 { id: ID('b1'), title: 'Design homepage', status: 'doing', project_id: ID('a1') },
216 { id: ID('b2'), title: 'Write copy', status: 'todo', project_id: ID('a1') },
217 { id: ID('b3'), title: 'Book movers', status: 'todo', project_id: ID('a2') },
218 ],
219 },
220 ],
221};
222
223export const TEMPLATES: Record<string, TemplateDef> = {
224 [contactsCrm.id]: contactsCrm,
225 [inventory.id]: inventory,
226 [bookings.id]: bookings,
227 [tasks.id]: tasks,
228};
229
230/** Public list for the picker UI (no internal detail). */
231export function listTemplates(): Array<Pick<TemplateDef, 'id' | 'name' | 'description' | 'icon'>> {
232 return Object.values(TEMPLATES).map(({ id, name, description, icon }) => ({
233 id,
234 name,
235 description,
236 icon,
237 }));
238}
239
240export function getTemplate(id: string): TemplateDef | undefined {
241 return TEMPLATES[id];
242}