thatcher 1.0.4
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/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- package/src/ui/widgets.js +152 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AnEntrypoint
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Thatcher SDK
|
|
2
|
+
|
|
3
|
+
**Configuration-Driven Application Framework for Data-Intensive Web Apps**
|
|
4
|
+
|
|
5
|
+
Thatcher is a complete extraction of the moonlanding application framework. Build full-featured CRUD applications with workflows, permissions, and external integrations — all through YAML configuration, without writing code.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Zero-Code CRUD** — Define entities in YAML; automatic REST API, UI, and database schema
|
|
10
|
+
- **Workflow Engine** — State machine with transitions, locks, and permissions
|
|
11
|
+
- **Role-Based Access Control** — Fine-grained permission templates per entity
|
|
12
|
+
- **Plugin System** — Extend entities with hooks, validators, and custom fields
|
|
13
|
+
- **External Integrations** — Google Drive/Gmail, Email (SMTP), PDF generation
|
|
14
|
+
- **Hot Reload** — Config changes reflect instantly without restart
|
|
15
|
+
- **Production-Ready** — SQLite, WAL mode, connection pooling, metrics, audit logging
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Install globally (or use bun x)
|
|
21
|
+
npm install -g thatcher
|
|
22
|
+
|
|
23
|
+
# 2. Generate starter config
|
|
24
|
+
thatcher example
|
|
25
|
+
|
|
26
|
+
# 3. Start the server
|
|
27
|
+
thatcher start
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or use directly with Bun:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun x thatcher start
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Thatcher is driven by a single `thatcher.config.yml` file:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
# thatcher.config.yml
|
|
42
|
+
roles:
|
|
43
|
+
admin:
|
|
44
|
+
hierarchy: 0
|
|
45
|
+
label: Admin
|
|
46
|
+
permissions_scope: global
|
|
47
|
+
user:
|
|
48
|
+
hierarchy: 1
|
|
49
|
+
label: User
|
|
50
|
+
permissions_scope: assigned
|
|
51
|
+
|
|
52
|
+
permission_templates:
|
|
53
|
+
basic:
|
|
54
|
+
admin: [list, view, create, edit, delete, manage_settings]
|
|
55
|
+
user: [list, view]
|
|
56
|
+
|
|
57
|
+
entities:
|
|
58
|
+
item:
|
|
59
|
+
label: Item
|
|
60
|
+
label_plural: Items
|
|
61
|
+
fields:
|
|
62
|
+
name:
|
|
63
|
+
type: text
|
|
64
|
+
required: true
|
|
65
|
+
description:
|
|
66
|
+
type: textarea
|
|
67
|
+
status:
|
|
68
|
+
type: enum
|
|
69
|
+
options: [active, archived]
|
|
70
|
+
default: active
|
|
71
|
+
|
|
72
|
+
workflows:
|
|
73
|
+
simple:
|
|
74
|
+
stages:
|
|
75
|
+
- draft
|
|
76
|
+
- active
|
|
77
|
+
- completed
|
|
78
|
+
|
|
79
|
+
thresholds:
|
|
80
|
+
system:
|
|
81
|
+
pagination:
|
|
82
|
+
default_page_size: 20
|
|
83
|
+
max_page_size: 100
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
That's it. Start the server and you have:
|
|
87
|
+
- SQLite tables `item` with correct schema
|
|
88
|
+
- REST endpoints: `GET/POST/PUT/DELETE /api/item`
|
|
89
|
+
- Role-based permissions enforced
|
|
90
|
+
- State transitions via `POST /api/item/:id/transition`
|
|
91
|
+
|
|
92
|
+
## Programmatic Usage
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
import { createThatcher } from 'thatcher';
|
|
96
|
+
|
|
97
|
+
const thatcher = createThatcher({
|
|
98
|
+
config: './thatcher.config.yml',
|
|
99
|
+
databasePath: './data/app.db',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Initialize (loads config, migrates DB, registers plugins)
|
|
103
|
+
await thatcher.init();
|
|
104
|
+
|
|
105
|
+
// Start HTTP server
|
|
106
|
+
await thatcher.startServer({ port: 3000 });
|
|
107
|
+
|
|
108
|
+
// Use APIs directly
|
|
109
|
+
const items = await thatcher.list('item');
|
|
110
|
+
const item = await thatcher.get('item', 'id123');
|
|
111
|
+
const created = await thatcher.create('item', { name: 'Test' }, { id: 'user1' });
|
|
112
|
+
|
|
113
|
+
// Workflow transitions
|
|
114
|
+
await thatcher.transition('item', item.id, 'simple', 'active', { id: 'user1', role: 'admin' });
|
|
115
|
+
|
|
116
|
+
// Permissions
|
|
117
|
+
const canEdit = await thatcher.can(user, thatcher.getEntitySpec('item'), 'edit');
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## CLI Commands
|
|
121
|
+
|
|
122
|
+
| Command | Description |
|
|
123
|
+
|---------|-------------|
|
|
124
|
+
| `thatcher start` | Start server in production mode |
|
|
125
|
+
| `thatcher dev` | Start with hot reload enabled |
|
|
126
|
+
| `thatcher migrate` | Run database migrations only |
|
|
127
|
+
| `thatcher validate` | Validate configuration file |
|
|
128
|
+
| `thatcher console` | Open REPL with thatcher API |
|
|
129
|
+
| `thatcher example` | Generate example config |
|
|
130
|
+
|
|
131
|
+
## Architecture
|
|
132
|
+
|
|
133
|
+
### Core Components
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
thatcher/
|
|
137
|
+
├── src/
|
|
138
|
+
│ ├── index.js # Main entry: createThatcher()
|
|
139
|
+
│ ├── cli.js # CLI commands
|
|
140
|
+
│ ├── config/
|
|
141
|
+
│ │ ├── config-loader.js # YAML loading & validation
|
|
142
|
+
│ │ ├── spec-helpers.js # Entity spec utilities
|
|
143
|
+
│ │ ├── env.js # Environment config
|
|
144
|
+
│ │ └── constants.js # HTTP codes, statuses
|
|
145
|
+
│ ├── lib/
|
|
146
|
+
│ │ ├── database-core.js # SQLite init, migrations
|
|
147
|
+
│ │ ├── query-engine.js # Read operations (GET, search)
|
|
148
|
+
│ │ ├── query-engine-write.js # Write operations (CRUD)
|
|
149
|
+
│ │ ├── config-generator-engine.js # Spec builder
|
|
150
|
+
│ │ ├── hook-engine.js # Event system
|
|
151
|
+
│ │ ├── workflow-engine.js # State machines
|
|
152
|
+
│ │ ├── auth-middleware.js # Auth checks
|
|
153
|
+
│ │ ├── crud-factory.js # Handler factory
|
|
154
|
+
│ │ ├── crud-handlers.js # HTTP handlers
|
|
155
|
+
│ │ ├── validate.js # Validation
|
|
156
|
+
│ │ └── logger.js # Structured logging
|
|
157
|
+
│ ├── services/
|
|
158
|
+
│ │ └── permission.service.js # Authorization
|
|
159
|
+
│ ├── adapters/
|
|
160
|
+
│ │ ├── google-auth.js # Google OAuth
|
|
161
|
+
│ │ └── google-drive.js # Drive file operations
|
|
162
|
+
│ ├── plugins/
|
|
163
|
+
│ │ └── index.js # Plugin auto-discovery
|
|
164
|
+
│ └── server/
|
|
165
|
+
│ └── server.js # HTTP server
|
|
166
|
+
└── package.json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### How It Works
|
|
170
|
+
|
|
171
|
+
1. **Configuration Load** — `master-config.yml` parsed into memory
|
|
172
|
+
2. **Spec Generation** — For each entity, ConfigEngine builds full spec from base + overrides + plugins
|
|
173
|
+
3. **Database Migration** — Tables created/updated from spec fields (idempotent)
|
|
174
|
+
4. **Plugin Registration** — `.plugin.js` files extend entity behavior
|
|
175
|
+
5. **Request Handling** — Generic CRUD handlers enforce permissions, validate, execute hooks
|
|
176
|
+
|
|
177
|
+
### Entity Specification
|
|
178
|
+
|
|
179
|
+
Each entity derives from config:
|
|
180
|
+
|
|
181
|
+
| Config Key | Purpose |
|
|
182
|
+
|-----------|---------|
|
|
183
|
+
| `fields` | Column definitions (type, required, ref, enum, etc.) |
|
|
184
|
+
| `permission_template` | Maps roles → allowed actions |
|
|
185
|
+
| `workflow` | State machine name for lifecycle |
|
|
186
|
+
| `row_access` | Scoping: `team`, `assigned`, `client` |
|
|
187
|
+
| `list.defaultSort` | Default list ordering |
|
|
188
|
+
| `has_*` flags | UI features (PDF, collaboration, notifications) |
|
|
189
|
+
|
|
190
|
+
### Permissions
|
|
191
|
+
|
|
192
|
+
Permission templates define role capabilities per entity:
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
permission_templates:
|
|
196
|
+
standard:
|
|
197
|
+
admin: [list, view, create, edit, delete, export]
|
|
198
|
+
user: [list, view]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Actions: `list`, `view`, `create`, `edit`, `delete`, `archive`, `export`, `manage_settings`, etc.
|
|
202
|
+
|
|
203
|
+
Row access controls which records a user can see:
|
|
204
|
+
- `team` — Only records in user's team
|
|
205
|
+
- `assigned` — Only records assigned to user
|
|
206
|
+
- `client` — Only records for user's client
|
|
207
|
+
- `assigned_or_team` — Either condition
|
|
208
|
+
|
|
209
|
+
### Workflows
|
|
210
|
+
|
|
211
|
+
State machines with transitions:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
workflows:
|
|
215
|
+
engagement_lifecycle:
|
|
216
|
+
state_field: stage
|
|
217
|
+
stages:
|
|
218
|
+
- name: draft
|
|
219
|
+
label: Draft
|
|
220
|
+
forward: [review, submitted]
|
|
221
|
+
readonly: true
|
|
222
|
+
- name: review
|
|
223
|
+
label: In Review
|
|
224
|
+
forward: [approved, rejected]
|
|
225
|
+
backward: [draft]
|
|
226
|
+
requires_role: [manager, partner]
|
|
227
|
+
- name: closed
|
|
228
|
+
label: Closed
|
|
229
|
+
entry: partner_only
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Transitions validated automatically:
|
|
233
|
+
- Only allowed transitions
|
|
234
|
+
- Role requirements
|
|
235
|
+
- Lockout period (configurable)
|
|
236
|
+
- Readonly state blocks
|
|
237
|
+
|
|
238
|
+
### Hooks
|
|
239
|
+
|
|
240
|
+
Listen to lifecycle events:
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
// my-entity.plugin.js
|
|
244
|
+
export default {
|
|
245
|
+
entityName: 'item',
|
|
246
|
+
hooks: [
|
|
247
|
+
{
|
|
248
|
+
event: 'create:item:after',
|
|
249
|
+
handler: async ({ entity, id, data, user }) => {
|
|
250
|
+
console.log(`Item ${id} created by ${user.id}`);
|
|
251
|
+
// Send notification, sync external system, etc.
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Hook naming: `<timing>:<entity>:<phase>`
|
|
259
|
+
- `create:entity:before` / `after`
|
|
260
|
+
- `update:entity:before` / `after`
|
|
261
|
+
- `delete:entity:before` / `after`
|
|
262
|
+
- `transition:entity:before` / `after`
|
|
263
|
+
- Custom: `upload_files:entity:after`, `resolve_highlight:review:after`
|
|
264
|
+
|
|
265
|
+
### Plugins
|
|
266
|
+
|
|
267
|
+
Extend entities with custom fields and behavior:
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
// plugins/custom-item.plugin.js
|
|
271
|
+
export default {
|
|
272
|
+
entityName: 'item',
|
|
273
|
+
fields: {
|
|
274
|
+
custom_field: {
|
|
275
|
+
type: 'text',
|
|
276
|
+
label: 'Custom Field',
|
|
277
|
+
required: false,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
validators: {
|
|
281
|
+
validateCustom: (data) => {
|
|
282
|
+
if (data.custom_field && !data.custom_field.startsWith('X')) {
|
|
283
|
+
return 'Custom field must start with X';
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
All `.plugin.js` files in `plugins/` directory auto-loaded on startup.
|
|
292
|
+
|
|
293
|
+
## External Integrations
|
|
294
|
+
|
|
295
|
+
### Google OAuth & Drive
|
|
296
|
+
|
|
297
|
+
Set env vars:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
GOOGLE_CLIENT_ID=...
|
|
301
|
+
GOOGLE_CLIENT_SECRET=...
|
|
302
|
+
GOOGLE_SERVICE_ACCOUNT_PATH=./service-account.json
|
|
303
|
+
GOOGLE_DRIVE_FOLDER_ID=...
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Drive operations via `@/adapters/google-drive`:
|
|
307
|
+
|
|
308
|
+
```javascript
|
|
309
|
+
import { uploadFile, downloadFile, exportToPdf } from 'thatcher/adapters/google-drive';
|
|
310
|
+
|
|
311
|
+
await uploadFile('./local.pdf', 'Document.pdf', { folderId: '...' });
|
|
312
|
+
const pdf = await exportToPdf('docId');
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Email (SMTP)
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
EMAIL_HOST=smtp.gmail.com
|
|
319
|
+
EMAIL_PORT=587
|
|
320
|
+
EMAIL_USER=...
|
|
321
|
+
EMAIL_PASSWORD=...
|
|
322
|
+
EMAIL_FROM=noreply@example.com
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Use via `@/lib/email-sender` (included).
|
|
326
|
+
|
|
327
|
+
## API Reference
|
|
328
|
+
|
|
329
|
+
All APIs accessible via thatcher instance:
|
|
330
|
+
|
|
331
|
+
### CRUD
|
|
332
|
+
- `thatcher.list(entity, where, opts)` → Array
|
|
333
|
+
- `thatcher.get(entity, id)` → object
|
|
334
|
+
- `thatcher.create(entity, data, user)` → object
|
|
335
|
+
- `thatcher.update(entity, id, data, user)` → object
|
|
336
|
+
- `thatcher.delete(entity, id)` → void
|
|
337
|
+
|
|
338
|
+
### Search
|
|
339
|
+
- `thatcher.search(entity, query, where, opts)` → Array (uses FTS)
|
|
340
|
+
|
|
341
|
+
### Workflow
|
|
342
|
+
- `thatcher.transition(entityType, entityId, workflowName, toState, user, reason)` → object
|
|
343
|
+
- `thatcher.getAvailableTransitions(workflowName, currentState, user, record)` → Array
|
|
344
|
+
|
|
345
|
+
### AuthZ
|
|
346
|
+
- `thatcher.can(user, spec, action)` → boolean
|
|
347
|
+
- `thatcher.requirePermission(user, spec, action)` → throws if denied
|
|
348
|
+
|
|
349
|
+
### Config
|
|
350
|
+
- `thatcher.getConfigEngine()` → ConfigGeneratorEngine
|
|
351
|
+
- `thatcher.getEntitySpec(entityName)` → spec object
|
|
352
|
+
- `thatcher.getAllEntities()` → Array<string>
|
|
353
|
+
|
|
354
|
+
### Direct DB
|
|
355
|
+
- `thatcher.withTransaction(cb)` → Promise<result>
|
|
356
|
+
|
|
357
|
+
## Environment Variables
|
|
358
|
+
|
|
359
|
+
| Variable | Purpose | Default |
|
|
360
|
+
|----------|---------|---------|
|
|
361
|
+
| `PORT` | Server port | 3000 |
|
|
362
|
+
| `DATABASE_PATH` | SQLite file path | `./data/app.db` |
|
|
363
|
+
| `NODE_ENV` | `development` \| `production` | `development` |
|
|
364
|
+
| `DEBUG` | Enable debug logging | `false` |
|
|
365
|
+
| `GOOGLE_CLIENT_ID` | OAuth client ID | — |
|
|
366
|
+
| `GOOGLE_CLIENT_SECRET` | OAuth client secret | — |
|
|
367
|
+
| `GOOGLE_DRIVE_FOLDER_ID` | Root Drive folder | — |
|
|
368
|
+
| `EMAIL_HOST` | SMTP host | `smtp.gmail.com` |
|
|
369
|
+
| `EMAIL_PORT` | SMTP port | `587` |
|
|
370
|
+
| `EMAIL_USER` | SMTP username | — |
|
|
371
|
+
| `EMAIL_PASSWORD` | SMTP password | — |
|
|
372
|
+
|
|
373
|
+
## Migrating from Moonlanding
|
|
374
|
+
|
|
375
|
+
Thatcher is a drop-in replacement for moonlanding's core:
|
|
376
|
+
|
|
377
|
+
1. Copy `master-config.yml` → `thatcher.config.yml`
|
|
378
|
+
2. Move any `.plugin.js` files to new project's `plugins/`
|
|
379
|
+
3. Change import paths: `@/lib/...` → `thatcher/...`
|
|
380
|
+
4. Replace `import ... from '@/engine'` with `thatcher.*` methods
|
|
381
|
+
5. Update server initialization: `new Thatcher({ config: '...' })`
|
|
382
|
+
|
|
383
|
+
All business logic remains in config and plugins — no code changes required.
|
|
384
|
+
|
|
385
|
+
## Publishing
|
|
386
|
+
|
|
387
|
+
Thatcher is published to npm as `thatcher`. Every push to `main` triggers:
|
|
388
|
+
|
|
389
|
+
1. Bump version (from git tags or conventional commits)
|
|
390
|
+
2. Build & test
|
|
391
|
+
3. Publish to npm registry
|
|
392
|
+
4. Create GitHub release
|
|
393
|
+
|
|
394
|
+
CI/CD ready via included GitHub Actions workflow.
|
|
395
|
+
|
|
396
|
+
## License
|
|
397
|
+
|
|
398
|
+
MIT — Extracted from moonlanding with all functionality preserved.
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thatcher",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "A config-driven application framework for building data-intensive web apps without code. 100% feature-complete extraction from moonlanding.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"thatcher": "src/cli.js",
|
|
9
|
+
"thatcher-dev": "src/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "bun run src/cli.js dev",
|
|
13
|
+
"start": "bun run src/cli.js start",
|
|
14
|
+
"test": "bun test",
|
|
15
|
+
"lint": "eslint src/",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"framework",
|
|
20
|
+
"cms",
|
|
21
|
+
"low-code",
|
|
22
|
+
"config-driven",
|
|
23
|
+
"crud",
|
|
24
|
+
"workflow",
|
|
25
|
+
"sdk",
|
|
26
|
+
"bun",
|
|
27
|
+
"sqlite"
|
|
28
|
+
],
|
|
29
|
+
"author": "Extracted from moonlanding",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0",
|
|
33
|
+
"bun": ">=1.0.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@casl/ability": "^6.8.0",
|
|
37
|
+
"@lucia-auth/adapter-sqlite": "^3.0.2",
|
|
38
|
+
"arctic": "^1.9.2",
|
|
39
|
+
"bcrypt": "^5.1.1",
|
|
40
|
+
"better-sqlite3": "^11.10.0",
|
|
41
|
+
"compression": "^1.8.1",
|
|
42
|
+
"googleapis": "^140.0.0",
|
|
43
|
+
"js-yaml": "^4.1.1",
|
|
44
|
+
"lucia": "^3.2.2",
|
|
45
|
+
"nanoid": "^5.0.7",
|
|
46
|
+
"nodemailer": "^6.9.7",
|
|
47
|
+
"puppeteer": "^21.6.1",
|
|
48
|
+
"webjsx": "^0.0.73",
|
|
49
|
+
"tsx": "^4.7.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
53
|
+
"@types/node": "^20.16.5",
|
|
54
|
+
"eslint": "^8.56.0",
|
|
55
|
+
"typescript": "^5.3.3"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/AnEntrypoint/thatcher.git"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/AnEntrypoint/thatcher/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/AnEntrypoint/thatcher#readme",
|
|
65
|
+
"files": [
|
|
66
|
+
"src/**/*",
|
|
67
|
+
"README.md",
|
|
68
|
+
"LICENSE"
|
|
69
|
+
],
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Auth Adapter - Factory for Google OAuth2 clients
|
|
3
|
+
* Provides JWT and OAuth2 clients for Google APIs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { google } from 'googleapis';
|
|
7
|
+
import { buildConfig } from '../config/env.js';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
let _oauth2Client = null;
|
|
12
|
+
let _jwtClient = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize Google auth clients
|
|
16
|
+
* @param {object} [config] - Optional config override
|
|
17
|
+
*/
|
|
18
|
+
export function initGoogleAuth(config = null) {
|
|
19
|
+
const cfg = config || buildConfig();
|
|
20
|
+
const scopes = [
|
|
21
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
22
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
23
|
+
'https://www.googleapis.com/auth/drive',
|
|
24
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// OAuth2 client for web flow
|
|
28
|
+
_oauth2Client = new google.auth.OAuth2(
|
|
29
|
+
cfg.auth.google.clientId,
|
|
30
|
+
cfg.auth.google.clientSecret,
|
|
31
|
+
cfg.auth.google.redirectUri
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// JWT client for service account (server-to-server)
|
|
35
|
+
const serviceAccountPath = cfg.auth.google.serviceAccountPath || './config/service-account.json';
|
|
36
|
+
if (fs.existsSync(serviceAccountPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const key = JSON.parse(fs.readFileSync(serviceAccountPath, 'utf-8'));
|
|
39
|
+
_jwtClient = new google.auth.JWT({
|
|
40
|
+
email: key.client_email,
|
|
41
|
+
key: key.private_key,
|
|
42
|
+
scopes: [
|
|
43
|
+
'https://www.googleapis.com/auth/drive',
|
|
44
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn('[GoogleAuth] Failed to load service account:', err.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { oauth2: _oauth2Client, jwt: _jwtClient };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get OAuth2 client
|
|
57
|
+
* @returns {OAuth2Client|null}
|
|
58
|
+
*/
|
|
59
|
+
export function getOAuth2Client() {
|
|
60
|
+
if (!_oauth2Client) initGoogleAuth();
|
|
61
|
+
return _oauth2Client;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get JWT client (service account)
|
|
66
|
+
* @returns {JWT|null}
|
|
67
|
+
*/
|
|
68
|
+
export function getJWTClient() {
|
|
69
|
+
if (!_jwtClient) initGoogleAuth();
|
|
70
|
+
return _jwtClient;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate OAuth2 authorization URL
|
|
75
|
+
* @param {object} options - state, access_type, prompt, etc.
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function generateAuthUrl(options = {}) {
|
|
79
|
+
const client = getOAuth2Client();
|
|
80
|
+
if (!client) throw new Error('Google OAuth not configured');
|
|
81
|
+
|
|
82
|
+
const defaultScopes = [
|
|
83
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
84
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
return client.generateAuthUrl({
|
|
88
|
+
access_type: 'offline',
|
|
89
|
+
prompt: 'consent',
|
|
90
|
+
scope: defaultScopes,
|
|
91
|
+
...options,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Exchange code for tokens
|
|
97
|
+
* @param {string} code
|
|
98
|
+
* @returns {Promise<object>} tokens
|
|
99
|
+
*/
|
|
100
|
+
export async function exchangeCode(code) {
|
|
101
|
+
const client = getOAuth2Client();
|
|
102
|
+
if (!client) throw new Error('Google OAuth not configured');
|
|
103
|
+
|
|
104
|
+
const { tokens } = await client.getToken(code);
|
|
105
|
+
return tokens;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get user info from Google
|
|
110
|
+
* @param {string} accessToken
|
|
111
|
+
* @returns {Promise<object>}
|
|
112
|
+
*/
|
|
113
|
+
export async function getUserInfo(accessToken) {
|
|
114
|
+
const oauth2 = google.oauth2({ version: 'v2', auth: `Bearer ${accessToken}` });
|
|
115
|
+
const res = await oauth2.userinfo.get();
|
|
116
|
+
return res.data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Verify ID token
|
|
121
|
+
* @param {string} idToken
|
|
122
|
+
* @returns {Promise<object>}
|
|
123
|
+
*/
|
|
124
|
+
export async function verifyIdToken(idToken) {
|
|
125
|
+
const client = getOAuth2Client();
|
|
126
|
+
if (!client) throw new Error('Google OAuth not configured');
|
|
127
|
+
|
|
128
|
+
const ticket = await client.verifyIdToken({
|
|
129
|
+
idToken,
|
|
130
|
+
audience: null, // validate against any client ID (for multi-tenant)
|
|
131
|
+
});
|
|
132
|
+
return ticket.getPayload();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create OAuth2 client with refresh token
|
|
137
|
+
* @param {string} refreshToken
|
|
138
|
+
* @returns {OAuth2Client}
|
|
139
|
+
*/
|
|
140
|
+
export function createClientWithRefresh(refreshToken) {
|
|
141
|
+
const client = new google.auth.OAuth2(
|
|
142
|
+
buildConfig().auth.google.clientId,
|
|
143
|
+
buildConfig().auth.google.clientSecret,
|
|
144
|
+
buildConfig().auth.google.redirectUri
|
|
145
|
+
);
|
|
146
|
+
client.setCredentials({ refresh_token: refreshToken });
|
|
147
|
+
return client;
|
|
148
|
+
}
|