create-lego-one 2.0.12 → 2.0.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/dist/index.cjs +150 -15
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/template/.cursor/rules/rules.mdc +639 -0
- package/template/.dockerignore +58 -0
- package/template/.env.example +18 -0
- package/template/.eslintignore +5 -0
- package/template/.eslintrc.js +28 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc +11 -0
- package/template/CLAUDE.md +634 -0
- package/template/Dockerfile +67 -0
- package/template/PROMPT.md +457 -0
- package/template/README.md +325 -0
- package/template/docker-compose.yml +48 -0
- package/template/docker-entrypoint.sh +23 -0
- package/template/docs/checkpoints/.template.md +64 -0
- package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
- package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
- package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
- package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
- package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
- package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
- package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
- package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
- package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
- package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
- package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
- package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
- package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
- package/template/docs/framework/plans/00-index.md +164 -0
- package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
- package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
- package/template/docs/framework/plans/03-host-kernel.md +1518 -0
- package/template/docs/framework/plans/04-auth-system.md +1466 -0
- package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
- package/template/docs/framework/plans/06-ui-components.md +1478 -0
- package/template/docs/framework/plans/07-communication-system.md +1106 -0
- package/template/docs/framework/plans/08-plugin-system.md +1179 -0
- package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
- package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
- package/template/docs/framework/plans/11-testing.md +935 -0
- package/template/docs/framework/plans/12-deployment.md +896 -0
- package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
- package/template/docs/framework/research/00-modernjs-audit.md +488 -0
- package/template/docs/framework/research/01-system-blueprint.md +721 -0
- package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
- package/template/docs/framework/research/03-host-setup.md +714 -0
- package/template/docs/framework/research/04-plugin-architecture.md +645 -0
- package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
- package/template/docs/framework/research/06-cli-strategy.md +615 -0
- package/template/docs/framework/research/07-deployment.md +629 -0
- package/template/docs/framework/research/README.md +282 -0
- package/template/docs/framework/setup/00-index.md +210 -0
- package/template/docs/framework/setup/01-framework-structure.md +308 -0
- package/template/docs/framework/setup/02-development-workflow.md +405 -0
- package/template/docs/framework/setup/03-environment-setup.md +215 -0
- package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
- package/template/docs/framework/setup/05-plugin-system.md +620 -0
- package/template/docs/framework/setup/06-communication-patterns.md +451 -0
- package/template/docs/framework/setup/07-plugin-development.md +582 -0
- package/template/docs/framework/setup/08-component-library.md +658 -0
- package/template/docs/framework/setup/09-data-integration.md +609 -0
- package/template/docs/framework/setup/10-auth-rbac.md +497 -0
- package/template/docs/framework/setup/11-hooks-api.md +393 -0
- package/template/docs/framework/setup/12-components-api.md +665 -0
- package/template/docs/framework/setup/13-deployment-guide.md +566 -0
- package/template/docs/framework/setup/README.md +548 -0
- package/template/host/package.json +1 -1
- package/template/nginx.conf +72 -0
- package/template/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-todo/package.json +1 -1
- package/template/pocketbase/CHANGELOG.md +911 -0
- package/template/pocketbase/LICENSE.md +17 -0
- package/template/scripts/create-plugin.js +221 -0
- package/template/scripts/deploy.sh +56 -0
- package/template/tsconfig.base.json +26 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
# Data Migration Protocol: PocketBase Auto-Schema
|
|
2
|
+
|
|
3
|
+
**Project:** Lego-One (Modern.js SaaS OS)
|
|
4
|
+
**Document:** 02 - Data Migration Protocol
|
|
5
|
+
**Status:** Research Phase
|
|
6
|
+
|
|
7
|
+
## Executive Summary
|
|
8
|
+
|
|
9
|
+
This document defines the **Migration Protocol** for automatically synchronizing PocketBase collections when plugins are enabled. Since PocketBase lacks traditional SQL migrations, we use a **startup schema check** pattern that leverages the PocketBase Admin API to programmatically create collections.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Problem Statement
|
|
14
|
+
|
|
15
|
+
### 1.1 The Challenge
|
|
16
|
+
|
|
17
|
+
PocketBase manages collections through:
|
|
18
|
+
1. **Dashboard UI** - Manual creation/modification
|
|
19
|
+
2. **Admin API** - Programmatic operations (superuser only)
|
|
20
|
+
3. **JavaScript Hooks** - Server-side extensions
|
|
21
|
+
|
|
22
|
+
**Problem:** Each plugin requires specific database collections, but:
|
|
23
|
+
- Users shouldn't manually create collections via dashboard
|
|
24
|
+
- Plugins must be "drop-in" - work immediately when enabled
|
|
25
|
+
- Schema versioning is needed for plugin updates
|
|
26
|
+
|
|
27
|
+
### 1.2 Requirements
|
|
28
|
+
|
|
29
|
+
| Requirement | Description |
|
|
30
|
+
|-------------|-------------|
|
|
31
|
+
| **Auto-Discovery** | Host detects enabled plugins from `saas.config.ts` |
|
|
32
|
+
| **Schema Check** | Verify required collections exist before app starts |
|
|
33
|
+
| **Auto-Creation** | Create missing collections programmatically |
|
|
34
|
+
| **Idempotency** | Safe to run multiple times without errors |
|
|
35
|
+
| **Versioning** | Support schema migrations for plugin updates |
|
|
36
|
+
| **Multi-Tenancy** | Apply PocketBase API Rules for Row Level Security |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. Migration Architecture
|
|
41
|
+
|
|
42
|
+
### 2.1 Startup Protocol Flow
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ Host Application Boot │
|
|
47
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
48
|
+
│
|
|
49
|
+
▼
|
|
50
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
51
|
+
│ 1. Read saas.config.ts │
|
|
52
|
+
│ - Parse enabled plugins list │
|
|
53
|
+
│ - Load plugin.config.ts from each plugin │
|
|
54
|
+
└──────┬──────────────────────────────────────────────────────────────────┘
|
|
55
|
+
│
|
|
56
|
+
▼
|
|
57
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
58
|
+
│ 2. Initialize PocketBase Admin Client │
|
|
59
|
+
│ - Load admin credentials from env vars │
|
|
60
|
+
│ - Authenticate as superuser │
|
|
61
|
+
└──────┬──────────────────────────────────────────────────────────────────┘
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
65
|
+
│ 3. For each enabled plugin: │
|
|
66
|
+
│ a. Load migration files from plugin/migrations/ │
|
|
67
|
+
│ b. Check if collection exists ($app.findCollectionByNameOrId) │
|
|
68
|
+
│ c. If missing, create collection ($app.save(new Collection(...))) │
|
|
69
|
+
│ d. If exists, check schema version, migrate if needed │
|
|
70
|
+
└──────┬──────────────────────────────────────────────────────────────────┘
|
|
71
|
+
│
|
|
72
|
+
▼
|
|
73
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
74
|
+
│ 4. Store migration version in _lego_migrations table │
|
|
75
|
+
│ - Track which migrations have been applied │
|
|
76
|
+
│ - Prevent re-running same migrations │
|
|
77
|
+
└──────┬──────────────────────────────────────────────────────────────────┘
|
|
78
|
+
│
|
|
79
|
+
▼
|
|
80
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
81
|
+
│ 5. Proceed to app initialization │
|
|
82
|
+
│ - All required collections guaranteed to exist │
|
|
83
|
+
│ - Plugins can safely query their collections │
|
|
84
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2.2 Migration File Structure
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
packages/plugins/@lego/plugin-inventory/
|
|
91
|
+
├── migrations/
|
|
92
|
+
│ ├── 001_initial.ts # Initial schema
|
|
93
|
+
│ ├── 002_add_categories.ts # Feature addition
|
|
94
|
+
│ └── 003_add_suppliers.ts # Another feature
|
|
95
|
+
├── plugin.config.ts
|
|
96
|
+
└── src/
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 3. Implementation
|
|
102
|
+
|
|
103
|
+
### 3.1 Migration System Library
|
|
104
|
+
|
|
105
|
+
**File:** `host/src/kernel/migration-system.ts`
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import PocketBase from 'pocketbase';
|
|
109
|
+
import { Collection } from 'pocketbase';
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Types
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
export interface Migration {
|
|
116
|
+
version: number;
|
|
117
|
+
name: string;
|
|
118
|
+
collection: string;
|
|
119
|
+
up: () => Collection;
|
|
120
|
+
down?: () => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface PluginMigrations {
|
|
124
|
+
pluginName: string;
|
|
125
|
+
migrations: Migration[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface MigrationRecord {
|
|
129
|
+
id: string;
|
|
130
|
+
pluginName: string;
|
|
131
|
+
collection: string;
|
|
132
|
+
version: number;
|
|
133
|
+
executedAt: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Migration Runner
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
export class MigrationSystem {
|
|
141
|
+
private pb: PocketBase;
|
|
142
|
+
|
|
143
|
+
constructor(adminUrl: string, adminEmail: string, adminPassword: string) {
|
|
144
|
+
this.pb = new PocketBase(adminUrl);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async initialize(): Promise<void> {
|
|
148
|
+
// Authenticate as admin
|
|
149
|
+
await this.pb.admins.authWithPassword(
|
|
150
|
+
process.env.VITE_POCKETBASE_ADMIN_EMAIL!,
|
|
151
|
+
process.env.VITE_POCKETBASE_ADMIN_PASSWORD!
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Ensure migrations tracking table exists
|
|
155
|
+
await this.ensureMigrationsTable();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async ensureMigrationsTable(): Promise<void> {
|
|
159
|
+
// Check if _lego_migrations collection exists
|
|
160
|
+
const existing = await this.pb.collections.getList(1, 1, {
|
|
161
|
+
filter: `name = '_lego_migrations'`,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (existing.items.length === 0) {
|
|
165
|
+
// Create the migrations tracking collection
|
|
166
|
+
const migrationCollection = new Collection({
|
|
167
|
+
name: '_lego_migrations',
|
|
168
|
+
type: 'base',
|
|
169
|
+
listRule: null, // No list access via API
|
|
170
|
+
viewRule: null, // No view access via API
|
|
171
|
+
createRule: null, // No create access via API
|
|
172
|
+
updateRule: null, // No update access via API
|
|
173
|
+
deleteRule: null, // No delete access via API
|
|
174
|
+
fields: [
|
|
175
|
+
{
|
|
176
|
+
name: 'pluginName',
|
|
177
|
+
type: 'text',
|
|
178
|
+
required: true,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'collection',
|
|
182
|
+
type: 'text',
|
|
183
|
+
required: true,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'version',
|
|
187
|
+
type: 'number',
|
|
188
|
+
required: true,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'executedAt',
|
|
192
|
+
type: 'autodate',
|
|
193
|
+
required: true,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
indexes: [
|
|
197
|
+
'CREATE UNIQUE INDEX idx_migrations_plugin_collection_version ON _lego_migrations (pluginName, collection, version)',
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await this.executeAdminRequest('/api/collections', 'POST', migrationCollection);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check if a migration has already been executed
|
|
207
|
+
*/
|
|
208
|
+
private async isExecuted(pluginName: string, collection: string, version: number): Promise<boolean> {
|
|
209
|
+
try {
|
|
210
|
+
const records = await this.pb.records.getList('_lego_migrations', 1, 1, {
|
|
211
|
+
filter: `pluginName = "${pluginName}" && collection = "${collection}" && version = ${version}`,
|
|
212
|
+
});
|
|
213
|
+
return records.totalItems > 0;
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Record a migration as executed
|
|
221
|
+
*/
|
|
222
|
+
private async recordMigration(pluginName: string, collection: string, version: number): Promise<void> {
|
|
223
|
+
await this.pb.records.create('_lego_migrations', {
|
|
224
|
+
pluginName,
|
|
225
|
+
collection,
|
|
226
|
+
version,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Execute a PocketBase Admin API request
|
|
232
|
+
* Note: Collection operations require superuser privileges
|
|
233
|
+
*/
|
|
234
|
+
private async executeAdminRequest(endpoint: string, method: string, body?: any): Promise<any> {
|
|
235
|
+
const url = `${this.pb.baseUrl}${endpoint}`;
|
|
236
|
+
|
|
237
|
+
const response = await fetch(url, {
|
|
238
|
+
method,
|
|
239
|
+
headers: {
|
|
240
|
+
'Content-Type': 'application/json',
|
|
241
|
+
'Authorization': this.pb.authStore.token,
|
|
242
|
+
},
|
|
243
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
const error = await response.text();
|
|
248
|
+
throw new Error(`Admin API request failed: ${error}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return response.json();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if a collection exists
|
|
256
|
+
*/
|
|
257
|
+
async collectionExists(name: string): Promise<boolean> {
|
|
258
|
+
try {
|
|
259
|
+
// Try to fetch via Admin API
|
|
260
|
+
await this.executeAdminRequest(`/api/collections/${name}`, 'GET');
|
|
261
|
+
return true;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create a new collection from a migration
|
|
269
|
+
*/
|
|
270
|
+
async createCollection(collection: Collection): Promise<void> {
|
|
271
|
+
await this.executeAdminRequest('/api/collections', 'POST', collection);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update an existing collection
|
|
276
|
+
*/
|
|
277
|
+
async updateCollection(id: string, collection: Partial<Collection>): Promise<void> {
|
|
278
|
+
await this.executeAdminRequest(`/api/collections/${id}`, 'PATCH', collection);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Run migrations for a plugin
|
|
283
|
+
*/
|
|
284
|
+
async runMigrations(pluginMigrations: PluginMigrations): Promise<void> {
|
|
285
|
+
const { pluginName, migrations } = pluginMigrations;
|
|
286
|
+
|
|
287
|
+
// Sort migrations by version
|
|
288
|
+
const sortedMigrations = migrations.sort((a, b) => a.version - b.version);
|
|
289
|
+
|
|
290
|
+
for (const migration of sortedMigrations) {
|
|
291
|
+
const executed = await this.isExecuted(pluginName, migration.collection, migration.version);
|
|
292
|
+
|
|
293
|
+
if (!executed) {
|
|
294
|
+
console.log(`[Migration] Running ${pluginName}/${migration.name} (v${migration.version})`);
|
|
295
|
+
|
|
296
|
+
// Execute the migration
|
|
297
|
+
const collection = migration.up();
|
|
298
|
+
|
|
299
|
+
// Check if collection exists
|
|
300
|
+
const exists = await this.collectionExists(migration.collection);
|
|
301
|
+
|
|
302
|
+
if (exists) {
|
|
303
|
+
// Update existing collection
|
|
304
|
+
const existing = await this.executeAdminRequest(`/api/collections/${migration.collection}`, 'GET');
|
|
305
|
+
await this.updateCollection(existing.id, collection);
|
|
306
|
+
} else {
|
|
307
|
+
// Create new collection
|
|
308
|
+
await this.createCollection(collection);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Record as executed
|
|
312
|
+
await this.recordMigration(pluginName, migration.collection, migration.version);
|
|
313
|
+
|
|
314
|
+
console.log(`[Migration] Completed ${pluginName}/${migration.name}`);
|
|
315
|
+
} else {
|
|
316
|
+
console.log(`[Migration] Skipped ${pluginName}/${migration.name} (already executed)`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Run all pending migrations for multiple plugins
|
|
323
|
+
*/
|
|
324
|
+
async runAllMigrations(allMigrations: PluginMigrations[]): Promise<void> {
|
|
325
|
+
for (const pluginMigrations of allMigrations) {
|
|
326
|
+
await this.runMigrations(pluginMigrations);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 3.2 Plugin Migration Definition
|
|
333
|
+
|
|
334
|
+
**File:** `packages/plugins/@lego/plugin-inventory/migrations/001_initial.ts`
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { Collection } from 'pocketbase';
|
|
338
|
+
import { BoolField, NumberField, TextField, RelationField } from 'pocketbase';
|
|
339
|
+
|
|
340
|
+
export const version = 1;
|
|
341
|
+
export const name = 'initial';
|
|
342
|
+
|
|
343
|
+
export function up(): Collection {
|
|
344
|
+
return new Collection({
|
|
345
|
+
type: 'base',
|
|
346
|
+
name: 'inventory_items',
|
|
347
|
+
// Multi-tenancy: Users can only see their own items
|
|
348
|
+
listRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
349
|
+
viewRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
350
|
+
createRule: '@request.auth.id != ""',
|
|
351
|
+
updateRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
352
|
+
deleteRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
353
|
+
fields: [
|
|
354
|
+
{
|
|
355
|
+
name: 'name',
|
|
356
|
+
type: 'text',
|
|
357
|
+
required: true,
|
|
358
|
+
max: 200,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'description',
|
|
362
|
+
type: 'text',
|
|
363
|
+
required: false,
|
|
364
|
+
max: 1000,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'quantity',
|
|
368
|
+
type: 'number',
|
|
369
|
+
required: true,
|
|
370
|
+
min: 0,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'price',
|
|
374
|
+
type: 'number',
|
|
375
|
+
required: true,
|
|
376
|
+
min: 0,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: 'owner',
|
|
380
|
+
type: 'relation',
|
|
381
|
+
required: true,
|
|
382
|
+
maxSelect: 1,
|
|
383
|
+
collectionId: '_pb_users_auth_',
|
|
384
|
+
cascadeDelete: true,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
indexes: [
|
|
388
|
+
'CREATE INDEX idx_inventory_owner ON inventory_items (owner)',
|
|
389
|
+
],
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**File:** `packages/plugins/@lego/plugin-inventory/migrations/002_add_categories.ts`
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { Collection } from 'pocketbase';
|
|
398
|
+
import { SelectField } from 'pocketbase';
|
|
399
|
+
|
|
400
|
+
export const version = 2;
|
|
401
|
+
export const name = 'add_categories';
|
|
402
|
+
|
|
403
|
+
export function up(): Collection {
|
|
404
|
+
return new Collection({
|
|
405
|
+
type: 'base',
|
|
406
|
+
name: 'inventory_items',
|
|
407
|
+
// Preserve existing rules
|
|
408
|
+
listRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
409
|
+
viewRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
410
|
+
createRule: '@request.auth.id != ""',
|
|
411
|
+
updateRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
412
|
+
deleteRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
413
|
+
fields: [
|
|
414
|
+
// ... existing fields from 001_initial ...
|
|
415
|
+
{
|
|
416
|
+
name: 'name',
|
|
417
|
+
type: 'text',
|
|
418
|
+
required: true,
|
|
419
|
+
max: 200,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'quantity',
|
|
423
|
+
type: 'number',
|
|
424
|
+
required: true,
|
|
425
|
+
min: 0,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'price',
|
|
429
|
+
type: 'number',
|
|
430
|
+
required: true,
|
|
431
|
+
min: 0,
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: 'owner',
|
|
435
|
+
type: 'relation',
|
|
436
|
+
required: true,
|
|
437
|
+
maxSelect: 1,
|
|
438
|
+
collectionId: '_pb_users_auth_',
|
|
439
|
+
cascadeDelete: true,
|
|
440
|
+
},
|
|
441
|
+
// NEW FIELD
|
|
442
|
+
{
|
|
443
|
+
name: 'category',
|
|
444
|
+
type: 'select',
|
|
445
|
+
required: false,
|
|
446
|
+
values: ['electronics', 'clothing', 'food', 'other'],
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
indexes: [
|
|
450
|
+
'CREATE INDEX idx_inventory_owner ON inventory_items (owner)',
|
|
451
|
+
'CREATE INDEX idx_inventory_category ON inventory_items (category)',
|
|
452
|
+
],
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 3.3 Plugin Config: Expose Migrations
|
|
458
|
+
|
|
459
|
+
**File:** `packages/plugins/@lego/plugin-inventory/plugin.config.ts`
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { definePluginConfig } from '@lego/kernel/plugin-config';
|
|
463
|
+
|
|
464
|
+
export default definePluginConfig({
|
|
465
|
+
name: '@lego/plugin-inventory',
|
|
466
|
+
version: '1.0.0',
|
|
467
|
+
displayName: 'Inventory Management',
|
|
468
|
+
description: 'Track inventory items and stock levels',
|
|
469
|
+
|
|
470
|
+
// Expose migrations to host
|
|
471
|
+
migrations: {
|
|
472
|
+
collection: 'inventory_items',
|
|
473
|
+
files: [
|
|
474
|
+
'./migrations/001_initial',
|
|
475
|
+
'./migrations/002_add_categories',
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
// ... other config ...
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### 3.4 Host: Load and Run Migrations
|
|
484
|
+
|
|
485
|
+
**File:** `host/src/kernel/migration-loader.ts`
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { MigrationSystem, PluginMigrations } from './migration-system';
|
|
489
|
+
import { loadSaaSConfig } from './plugin-loader';
|
|
490
|
+
|
|
491
|
+
export async function loadAndRunMigrations(): Promise<void> {
|
|
492
|
+
// Initialize migration system
|
|
493
|
+
const migrationSystem = new MigrationSystem(
|
|
494
|
+
import.meta.env.VITE_POCKETBASE_URL,
|
|
495
|
+
import.meta.env.VITE_POCKETBASE_ADMIN_EMAIL,
|
|
496
|
+
import.meta.env.VITE_POCKETBASE_ADMIN_PASSWORD
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
await migrationSystem.initialize();
|
|
500
|
+
|
|
501
|
+
// Load enabled plugins
|
|
502
|
+
const enabledPlugins = await loadSaaSConfig();
|
|
503
|
+
|
|
504
|
+
// Load migrations from each plugin
|
|
505
|
+
const allMigrations: PluginMigrations[] = [];
|
|
506
|
+
|
|
507
|
+
for (const plugin of enabledPlugins) {
|
|
508
|
+
try {
|
|
509
|
+
// Dynamically import plugin config
|
|
510
|
+
const pluginConfig = await import(/* @vite-ignore */ `${plugin.entry}/plugin.config.ts`);
|
|
511
|
+
|
|
512
|
+
if (pluginConfig.default?.migrations) {
|
|
513
|
+
const migrations = [];
|
|
514
|
+
|
|
515
|
+
for (const migrationFile of pluginConfig.default.migrations.files) {
|
|
516
|
+
const migrationModule = await import(/* @vite-ignore */ migrationFile);
|
|
517
|
+
migrations.push({
|
|
518
|
+
version: migrationModule.version,
|
|
519
|
+
name: migrationModule.name,
|
|
520
|
+
collection: pluginConfig.default.migrations.collection,
|
|
521
|
+
up: migrationModule.up,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
allMigrations.push({
|
|
526
|
+
pluginName: plugin.name,
|
|
527
|
+
migrations,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.warn(`[Migration] Could not load migrations for ${plugin.name}:`, error);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Run all pending migrations
|
|
536
|
+
await migrationSystem.runAllMigrations(allMigrations);
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 3.5 Host: Run Migrations on Bootstrap
|
|
541
|
+
|
|
542
|
+
**File:** `host/src/bootstrap.tsx`
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
import { StrictMode } from 'react';
|
|
546
|
+
import ReactDOM from 'react-dom';
|
|
547
|
+
import App from './App';
|
|
548
|
+
import { loadAndRunMigrations } from './kernel/migration-loader';
|
|
549
|
+
import { registerSharedState } from './kernel/shared-state-bridge';
|
|
550
|
+
import './styles/globals.css';
|
|
551
|
+
|
|
552
|
+
async function bootstrap() {
|
|
553
|
+
// 1. Run database migrations first
|
|
554
|
+
console.log('[Boot] Running database migrations...');
|
|
555
|
+
try {
|
|
556
|
+
await loadAndRunMigrations();
|
|
557
|
+
console.log('[Boot] Migrations complete');
|
|
558
|
+
} catch (error) {
|
|
559
|
+
console.error('[Boot] Migration failed:', error);
|
|
560
|
+
// You might want to block app startup or show error UI
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// 2. Register shared state bridge
|
|
565
|
+
registerSharedState();
|
|
566
|
+
|
|
567
|
+
// 3. Mount React app
|
|
568
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
569
|
+
<StrictMode>
|
|
570
|
+
<App />
|
|
571
|
+
</StrictMode>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
bootstrap();
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## 4. PocketBase API Rules for Multi-Tenancy
|
|
581
|
+
|
|
582
|
+
### 4.1 Row Level Security Pattern
|
|
583
|
+
|
|
584
|
+
All plugin collections must follow this pattern:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
const collection = new Collection({
|
|
588
|
+
name: 'plugin_data',
|
|
589
|
+
// Users can only list their own records
|
|
590
|
+
listRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
591
|
+
// Users can only view their own records
|
|
592
|
+
viewRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
593
|
+
// Authenticated users can create
|
|
594
|
+
createRule: '@request.auth.id != ""',
|
|
595
|
+
// Users can only update their own records
|
|
596
|
+
updateRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
597
|
+
// Users can only delete their own records
|
|
598
|
+
deleteRule: '@request.auth.id != "" && owner = @request.auth.id',
|
|
599
|
+
fields: [
|
|
600
|
+
// ...
|
|
601
|
+
{
|
|
602
|
+
name: 'owner',
|
|
603
|
+
type: 'relation',
|
|
604
|
+
required: true,
|
|
605
|
+
maxSelect: 1,
|
|
606
|
+
collectionId: '_pb_users_auth_',
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
});
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### 4.2 Admin-Only Collections
|
|
613
|
+
|
|
614
|
+
For settings that apply across all users (app-level config):
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
const adminCollection = new Collection({
|
|
618
|
+
name: 'plugin_settings',
|
|
619
|
+
// Only admins can access
|
|
620
|
+
listRule: 'id = @request.auth.id && @collection.permissions = "admin"',
|
|
621
|
+
viewRule: 'id = @request.auth.id && @collection.permissions = "admin"',
|
|
622
|
+
createRule: 'id = @request.auth.id && @collection.permissions = "admin"',
|
|
623
|
+
updateRule: 'id = @request.auth.id && @collection.permissions = "admin"',
|
|
624
|
+
deleteRule: 'id = @request.auth.id && @collection.permissions = "admin"',
|
|
625
|
+
fields: [ /* ... */ ],
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## 5. Best Practices
|
|
632
|
+
|
|
633
|
+
### 5.1 Migration Guidelines
|
|
634
|
+
|
|
635
|
+
| Practice | Description |
|
|
636
|
+
|----------|-------------|
|
|
637
|
+
| **Version Numbers** | Use sequential integers (001, 002, 003...) |
|
|
638
|
+
| **Idempotency** | Migrations should be safe to re-run |
|
|
639
|
+
| **Backwards Compatible** | New migrations shouldn't break existing data |
|
|
640
|
+
| **Test Locally** | Test migrations against a fresh PocketBase instance |
|
|
641
|
+
| **Document Changes** | Add comments explaining schema changes |
|
|
642
|
+
|
|
643
|
+
### 5.2 Error Handling
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
// Always wrap migration execution in try-catch
|
|
647
|
+
try {
|
|
648
|
+
await loadAndRunMigrations();
|
|
649
|
+
} catch (error) {
|
|
650
|
+
if (error.message.includes('authentication')) {
|
|
651
|
+
showErrorDialog('Database authentication failed. Check admin credentials.');
|
|
652
|
+
} else if (error.message.includes('collection')) {
|
|
653
|
+
showErrorDialog('Failed to create collection. Check migration syntax.');
|
|
654
|
+
} else {
|
|
655
|
+
showErrorDialog('Migration failed. Check console for details.');
|
|
656
|
+
}
|
|
657
|
+
throw error; // Prevent app from starting with invalid schema
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### 5.3 Development vs Production
|
|
662
|
+
|
|
663
|
+
**Development:**
|
|
664
|
+
```bash
|
|
665
|
+
# Start PocketBase with fresh data
|
|
666
|
+
pocketbase serve --dev
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**Production:**
|
|
670
|
+
- Back up PocketBase data directory before migrations
|
|
671
|
+
- Test migrations in staging first
|
|
672
|
+
- Keep `_lego_migrations` table for audit trail
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## 6. Alternative Approach: PB_MIGRATE Hook
|
|
677
|
+
|
|
678
|
+
For production deployments, consider using PocketBase's **migrate** hook:
|
|
679
|
+
|
|
680
|
+
**File:** `pocketbase/migrations/migrate.go` (Go - not covered in our TypeScript-only stack)
|
|
681
|
+
|
|
682
|
+
Since we're using a **TypeScript-only** approach with the Admin API, the startup protocol is preferred over Go hooks.
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## 7. Next Steps
|
|
687
|
+
|
|
688
|
+
1. **`03-host-setup.md`**: Initialize the Modern.js host app
|
|
689
|
+
2. **`04-plugin-architecture.md`**: Plugin development patterns
|
|
690
|
+
3. **`05-slot-injection-pattern.md`**: UI extension system
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## References
|
|
695
|
+
|
|
696
|
+
- [PocketBase JS Collections Documentation](https://pocketbase.io/docs/js-collections/)
|
|
697
|
+
- [PocketBase API Rules Guide](https://pocketbase.io/docs/collections/)
|
|
698
|
+
- [PocketBase Admin API](https://pocketbase.io/docs/api-collections/)
|
|
699
|
+
- [GitHub Discussion: Programmatically Create Collections](https://github.com/pocketbase/pocketbase/discussions/890)
|