chore: import codes
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
# Module System
|
||||
|
||||
Modules are self-contained features that can be enabled/disabled via environment variables.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/modules/your-module/
|
||||
├── module.config.js # Required — navigation, pages, widgets
|
||||
├── db.js # Database schema (createTables / dropTables)
|
||||
├── crud.js # CRUD operations
|
||||
├── actions.js # Server actions (for public pages)
|
||||
├── metadata.js # SEO metadata generators
|
||||
├── api.js # API route handlers
|
||||
├── cron.config.js # Scheduled tasks
|
||||
├── index.js # Public API re-exports
|
||||
├── .env.example # Environment variable documentation
|
||||
├── admin/ # Admin pages (lazy-loaded)
|
||||
│ └── index.js # Re-exports admin components
|
||||
├── pages/ # Public pages (lazy-loaded)
|
||||
│ └── index.js
|
||||
├── dashboard/ # Dashboard widgets
|
||||
│ ├── statsActions.js
|
||||
│ └── Widget.js
|
||||
└── sub-feature/ # Optional sub-modules (e.g. items/, categories/)
|
||||
├── db.js
|
||||
├── crud.js
|
||||
└── admin/
|
||||
```
|
||||
|
||||
> Not all files are required. Only create what the module actually needs.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Create `module.config.js`
|
||||
|
||||
```javascript
|
||||
import { lazy } from 'react';
|
||||
|
||||
export default {
|
||||
// Module identity
|
||||
name: 'your-module',
|
||||
displayName: 'Your Module',
|
||||
version: '1.0.0',
|
||||
description: 'Description of your module',
|
||||
|
||||
// Other modules this one depends on (must be enabled too)
|
||||
dependencies: ['clients'],
|
||||
|
||||
// Environment variables this module uses (documentation only)
|
||||
envVars: [
|
||||
'YOUR_MODULE_API_KEY',
|
||||
],
|
||||
|
||||
// Admin navigation — single section object or array of section objects
|
||||
navigation: {
|
||||
id: 'your-module',
|
||||
title: 'Your Module',
|
||||
icon: 'SomeIcon', // String icon name from shared/Icons.js
|
||||
items: [
|
||||
{ name: 'Items', href: '/admin/your-module/items', icon: 'PackageIcon' },
|
||||
{ name: 'New', href: '/admin/your-module/items/new', icon: 'PlusSignIcon' },
|
||||
],
|
||||
},
|
||||
|
||||
// Admin pages — path → lazy component
|
||||
adminPages: {
|
||||
'/admin/your-module/items': lazy(() => import('./admin/ItemsListPage.js')),
|
||||
'/admin/your-module/items/new': lazy(() => import('./admin/ItemCreatePage.js')),
|
||||
'/admin/your-module/items/edit': lazy(() => import('./admin/ItemEditPage.js')),
|
||||
},
|
||||
|
||||
// (Optional) Custom resolver for dynamic paths not known at build time.
|
||||
// Called before the adminPages map. Return the lazy component or null.
|
||||
pageResolver(path) {
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
// example: /admin/your-module/{type}/list
|
||||
if (parts[2] === 'list') return lazy(() => import('./admin/ItemsListPage.js'));
|
||||
return null;
|
||||
},
|
||||
|
||||
// Public pages — keyed by 'default' (one component handles all public routes)
|
||||
publicPages: {
|
||||
default: lazy(() => import('./pages/YourModulePublicPages.js')),
|
||||
},
|
||||
|
||||
// Public route patterns for SEO/route matching (relative to /zen/your-module/)
|
||||
publicRoutes: [
|
||||
{ pattern: ':id', description: 'View item' },
|
||||
{ pattern: ':id/pdf', description: 'PDF viewer' },
|
||||
],
|
||||
|
||||
// Dashboard widgets (lazy-loaded, rendered on the admin dashboard)
|
||||
dashboardWidgets: [
|
||||
lazy(() => import('./dashboard/Widget.js')),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Navigation as multiple sections
|
||||
|
||||
When a module provides several distinct sections (like the `posts` module with one section per post type), set `navigation` to an array:
|
||||
|
||||
```javascript
|
||||
navigation: [
|
||||
{ id: 'your-module-foo', title: 'Foo', icon: 'Book02Icon', items: [...] },
|
||||
{ id: 'your-module-bar', title: 'Bar', icon: 'Layers01Icon', items: [...] },
|
||||
],
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Create `db.js`
|
||||
|
||||
Every module that uses a database must expose a `createTables` function:
|
||||
|
||||
```javascript
|
||||
import { query } from '@hykocx/zen/database';
|
||||
|
||||
export async function createTables() {
|
||||
const created = [];
|
||||
const skipped = [];
|
||||
|
||||
const exists = await query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = $1
|
||||
)`, ['zen_your_module']);
|
||||
|
||||
if (!exists.rows[0].exists) {
|
||||
await query(`
|
||||
CREATE TABLE zen_your_module (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
created.push('zen_your_module');
|
||||
} else {
|
||||
skipped.push('zen_your_module');
|
||||
}
|
||||
|
||||
return { created, skipped };
|
||||
}
|
||||
|
||||
export async function dropTables() {
|
||||
await query(`DROP TABLE IF EXISTS zen_your_module CASCADE`);
|
||||
}
|
||||
```
|
||||
|
||||
> **Never create migrations.** Instead, provide the SQL to the user so they can run it manually.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Create `.env.example`
|
||||
|
||||
Document every environment variable the module reads:
|
||||
|
||||
```bash
|
||||
#################################
|
||||
# MODULE YOUR-MODULE
|
||||
ZEN_MODULE_YOUR_MODULE=false
|
||||
|
||||
ZEN_MODULE_YOUR_MODULE_API_KEY=
|
||||
ZEN_MODULE_YOUR_MODULE_SOME_OPTION=default_value
|
||||
#################################
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Create `cron.config.js` (optional)
|
||||
|
||||
Only needed if the module requires scheduled tasks:
|
||||
|
||||
```javascript
|
||||
import { doSomething } from './reminders.js';
|
||||
|
||||
export default {
|
||||
jobs: [
|
||||
{
|
||||
name: 'your-module-task',
|
||||
description: 'Description of what this job does',
|
||||
schedule: '*/5 * * * *', // cron expression
|
||||
handler: doSomething,
|
||||
timezone: process.env.ZEN_TIMEZONE || 'America/Toronto',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Register the module in 5 files
|
||||
|
||||
### `modules/modules.registry.js` — add the module name
|
||||
|
||||
```javascript
|
||||
export const AVAILABLE_MODULES = [
|
||||
'clients',
|
||||
'invoice',
|
||||
'your-module',
|
||||
];
|
||||
```
|
||||
|
||||
### `modules/modules.pages.js` — import the config
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import yourModuleConfig from './your-module/module.config.js';
|
||||
|
||||
const MODULE_CONFIGS = {
|
||||
// ...existing modules...
|
||||
'your-module': yourModuleConfig,
|
||||
};
|
||||
```
|
||||
|
||||
### `modules/modules.actions.js` — import server actions (if public pages or dashboard widgets)
|
||||
|
||||
```javascript
|
||||
import { yourPublicAction } from './your-module/actions.js';
|
||||
import { getYourModuleDashboardStats } from './your-module/dashboard/statsActions.js';
|
||||
|
||||
export const MODULE_ACTIONS = {
|
||||
// ...existing modules...
|
||||
'your-module': { yourPublicAction },
|
||||
};
|
||||
|
||||
export const MODULE_DASHBOARD_ACTIONS = {
|
||||
// ...existing modules...
|
||||
'your-module': getYourModuleDashboardStats,
|
||||
};
|
||||
```
|
||||
|
||||
### `modules/modules.metadata.js` — import metadata generators (if SEO needed)
|
||||
|
||||
```javascript
|
||||
import * as yourModuleMetadata from './your-module/metadata.js';
|
||||
|
||||
export const MODULE_METADATA = {
|
||||
// ...existing modules...
|
||||
'your-module': yourModuleMetadata,
|
||||
};
|
||||
```
|
||||
|
||||
### `modules/init.js` — register the database initializer
|
||||
|
||||
```javascript
|
||||
import { createTables as createYourModuleTables } from './your-module/db.js';
|
||||
|
||||
const MODULE_DB_INITIALIZERS = {
|
||||
// ...existing modules...
|
||||
'your-module': createYourModuleTables,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Enable the module
|
||||
|
||||
```bash
|
||||
ZEN_MODULE_YOUR_MODULE=true
|
||||
```
|
||||
|
||||
The environment variable is derived from the module name: `ZEN_MODULE_` + module name uppercased (hyphens become underscores).
|
||||
|
||||
---
|
||||
|
||||
## Sub-modules
|
||||
|
||||
For complex modules, group related features into sub-directories. Each sub-module follows the same pattern (`db.js`, `crud.js`, `admin/`). The parent `module.config.js` registers all sub-module admin pages, and the parent `index.js` re-exports everything publicly.
|
||||
|
||||
See `src/modules/invoice/` for a complete example with `items/`, `categories/`, `transactions/`, and `recurrences/` sub-modules.
|
||||
|
||||
---
|
||||
|
||||
## Reference implementations
|
||||
|
||||
| Module | Features demonstrated |
|
||||
|--------|-----------------------|
|
||||
| `src/modules/invoice/` | Sub-modules, public pages, cron jobs, Stripe, email, PDF, dashboard widgets, metadata |
|
||||
| `src/modules/posts/` | Dynamic config from env vars, `pageResolver`, multiple navigation sections |
|
||||
| `src/modules/clients/` | Simple module, dependencies, no public pages |
|
||||
Reference in New Issue
Block a user