webspresso 0.0.5 → 0.0.7
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/README.md +564 -0
- package/bin/webspresso.js +255 -0
- package/core/applySchema.js +49 -0
- package/core/compileSchema.js +69 -0
- package/core/orm/eager-loader.js +232 -0
- package/core/orm/index.js +148 -0
- package/core/orm/migrations/index.js +205 -0
- package/core/orm/migrations/scaffold.js +312 -0
- package/core/orm/model.js +178 -0
- package/core/orm/query-builder.js +430 -0
- package/core/orm/repository.js +346 -0
- package/core/orm/schema-helpers.js +416 -0
- package/core/orm/scopes.js +183 -0
- package/core/orm/seeder.js +585 -0
- package/core/orm/transaction.js +69 -0
- package/core/orm/types.js +237 -0
- package/core/orm/utils.js +127 -0
- package/index.js +13 -1
- package/package.json +29 -5
- package/src/plugin-manager.js +2 -0
- package/utils/schemaCache.js +60 -0
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
|
|
|
7
7
|
- **File-Based Routing**: Create pages by adding `.njk` files to a `pages/` directory
|
|
8
8
|
- **Dynamic Routes**: Use `[param]` for dynamic params and `[...rest]` for catch-all routes
|
|
9
9
|
- **API Endpoints**: Add `.js` files to `pages/api/` with method suffixes (e.g., `health.get.js`)
|
|
10
|
+
- **Schema Validation**: Zod-based request validation for body, params, and query
|
|
10
11
|
- **Built-in i18n**: JSON-based translations with automatic locale detection
|
|
11
12
|
- **Lifecycle Hooks**: Global and route-level hooks for request processing
|
|
12
13
|
- **Template Helpers**: Laravel-inspired helper functions available in templates
|
|
@@ -494,6 +495,75 @@ Create `.js` files in `pages/api/` with optional method suffixes:
|
|
|
494
495
|
| `pages/api/echo.post.js` | `POST /api/echo` |
|
|
495
496
|
| `pages/api/users/[id].get.js` | `GET /api/users/:id` |
|
|
496
497
|
|
|
498
|
+
**Basic API Handler:**
|
|
499
|
+
|
|
500
|
+
```javascript
|
|
501
|
+
// pages/api/health.get.js
|
|
502
|
+
module.exports = async function handler(req, res) {
|
|
503
|
+
res.json({ status: 'ok' });
|
|
504
|
+
};
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**With Schema Validation:**
|
|
508
|
+
|
|
509
|
+
```javascript
|
|
510
|
+
// pages/api/posts.post.js
|
|
511
|
+
module.exports = {
|
|
512
|
+
schema: ({ z }) => ({
|
|
513
|
+
body: z.object({
|
|
514
|
+
title: z.string().min(3).max(100),
|
|
515
|
+
content: z.string(),
|
|
516
|
+
tags: z.array(z.string()).optional()
|
|
517
|
+
}),
|
|
518
|
+
query: z.object({
|
|
519
|
+
draft: z.coerce.boolean().default(false)
|
|
520
|
+
})
|
|
521
|
+
}),
|
|
522
|
+
|
|
523
|
+
async handler(req, res) {
|
|
524
|
+
// Validated & parsed data available in req.input
|
|
525
|
+
const { title, content, tags } = req.input.body;
|
|
526
|
+
const { draft } = req.input.query;
|
|
527
|
+
|
|
528
|
+
// Original req.body, req.query remain untouched
|
|
529
|
+
res.json({ success: true, title, draft });
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Schema Options:**
|
|
535
|
+
|
|
536
|
+
| Key | Description |
|
|
537
|
+
|-----|-------------|
|
|
538
|
+
| `body` | Validates `req.body` (POST/PUT/PATCH) |
|
|
539
|
+
| `params` | Validates route parameters (e.g., `:id`) |
|
|
540
|
+
| `query` | Validates query string parameters |
|
|
541
|
+
| `response` | Response schema (for documentation, not enforced) |
|
|
542
|
+
|
|
543
|
+
All schemas use [Zod](https://zod.dev) for validation. Invalid requests throw a `ZodError` which can be caught by error handlers.
|
|
544
|
+
|
|
545
|
+
**Dynamic Route with Params Validation:**
|
|
546
|
+
|
|
547
|
+
```javascript
|
|
548
|
+
// pages/api/users/[id].get.js
|
|
549
|
+
module.exports = {
|
|
550
|
+
schema: ({ z }) => ({
|
|
551
|
+
params: z.object({
|
|
552
|
+
id: z.string().uuid()
|
|
553
|
+
}),
|
|
554
|
+
query: z.object({
|
|
555
|
+
fields: z.string().optional()
|
|
556
|
+
})
|
|
557
|
+
}),
|
|
558
|
+
|
|
559
|
+
async handler(req, res) {
|
|
560
|
+
const { id } = req.input.params; // Validated UUID
|
|
561
|
+
const user = await getUser(id);
|
|
562
|
+
res.json(user);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
```
|
|
566
|
+
|
|
497
567
|
### Route Config
|
|
498
568
|
|
|
499
569
|
Add a `.js` file alongside your `.njk` file to configure the route:
|
|
@@ -621,6 +691,500 @@ module.exports = {
|
|
|
621
691
|
| `DEFAULT_LOCALE` | `en` | Default locale |
|
|
622
692
|
| `SUPPORTED_LOCALES` | `en` | Comma-separated locales |
|
|
623
693
|
| `BASE_URL` | `http://localhost:3000` | Base URL for canonical URLs |
|
|
694
|
+
| `DATABASE_URL` | - | Database connection string (for ORM) |
|
|
695
|
+
|
|
696
|
+
## ORM (Database)
|
|
697
|
+
|
|
698
|
+
Webspresso includes a minimal, Eloquent-inspired ORM built on Knex with Zod schemas as the single source of truth.
|
|
699
|
+
|
|
700
|
+
### Quick Start
|
|
701
|
+
|
|
702
|
+
```javascript
|
|
703
|
+
const { z } = require('zod');
|
|
704
|
+
const { createSchemaHelpers, defineModel, createDatabase } = require('webspresso');
|
|
705
|
+
|
|
706
|
+
// 1. Create schema helpers
|
|
707
|
+
const zdb = createSchemaHelpers(z);
|
|
708
|
+
|
|
709
|
+
// 2. Define your schema with database metadata
|
|
710
|
+
const UserSchema = z.object({
|
|
711
|
+
id: zdb.id(),
|
|
712
|
+
email: zdb.string({ unique: true, index: true }),
|
|
713
|
+
name: zdb.string({ maxLength: 100 }),
|
|
714
|
+
status: zdb.enum(['active', 'inactive'], { default: 'active' }),
|
|
715
|
+
company_id: zdb.foreignKey('companies', { nullable: true }),
|
|
716
|
+
created_at: zdb.timestamp({ auto: 'create' }),
|
|
717
|
+
updated_at: zdb.timestamp({ auto: 'update' }),
|
|
718
|
+
deleted_at: zdb.timestamp({ nullable: true }),
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// 3. Define your model
|
|
722
|
+
const User = defineModel({
|
|
723
|
+
name: 'User',
|
|
724
|
+
table: 'users',
|
|
725
|
+
schema: UserSchema,
|
|
726
|
+
relations: {
|
|
727
|
+
company: { type: 'belongsTo', model: () => Company, foreignKey: 'company_id' },
|
|
728
|
+
posts: { type: 'hasMany', model: () => Post, foreignKey: 'user_id' },
|
|
729
|
+
},
|
|
730
|
+
scopes: { softDelete: true, timestamps: true },
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// 4. Create database and use
|
|
734
|
+
const db = createDatabase({
|
|
735
|
+
client: 'pg',
|
|
736
|
+
connection: process.env.DATABASE_URL,
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const UserRepo = db.createRepository(User);
|
|
740
|
+
const user = await UserRepo.findById(1, { with: ['company', 'posts'] });
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### Schema Helpers (zdb)
|
|
744
|
+
|
|
745
|
+
The `zdb` helpers wrap Zod schemas with database column metadata:
|
|
746
|
+
|
|
747
|
+
| Helper | Description | Options |
|
|
748
|
+
|--------|-------------|---------|
|
|
749
|
+
| `zdb.id()` | Primary key (bigint, auto-increment) | |
|
|
750
|
+
| `zdb.uuid()` | UUID primary key | |
|
|
751
|
+
| `zdb.string(opts)` | VARCHAR column | `maxLength`, `unique`, `index`, `nullable` |
|
|
752
|
+
| `zdb.text(opts)` | TEXT column | `nullable` |
|
|
753
|
+
| `zdb.integer(opts)` | INTEGER column | `nullable`, `default` |
|
|
754
|
+
| `zdb.bigint(opts)` | BIGINT column | `nullable` |
|
|
755
|
+
| `zdb.float(opts)` | FLOAT column | `nullable` |
|
|
756
|
+
| `zdb.decimal(opts)` | DECIMAL column | `precision`, `scale`, `nullable` |
|
|
757
|
+
| `zdb.boolean(opts)` | BOOLEAN column | `default`, `nullable` |
|
|
758
|
+
| `zdb.date(opts)` | DATE column | `nullable` |
|
|
759
|
+
| `zdb.datetime(opts)` | DATETIME column | `nullable` |
|
|
760
|
+
| `zdb.timestamp(opts)` | TIMESTAMP column | `auto: 'create'\|'update'`, `nullable` |
|
|
761
|
+
| `zdb.json(opts)` | JSON column | `nullable` |
|
|
762
|
+
| `zdb.enum(values, opts)` | ENUM column | `default`, `nullable` |
|
|
763
|
+
| `zdb.foreignKey(table, opts)` | Foreign key (bigint) | `referenceColumn`, `nullable` |
|
|
764
|
+
| `zdb.foreignUuid(table, opts)` | Foreign key (uuid) | `referenceColumn`, `nullable` |
|
|
765
|
+
|
|
766
|
+
### Model Definition
|
|
767
|
+
|
|
768
|
+
```javascript
|
|
769
|
+
const User = defineModel({
|
|
770
|
+
name: 'User', // Model name
|
|
771
|
+
table: 'users', // Database table
|
|
772
|
+
schema: UserSchema, // Zod schema
|
|
773
|
+
primaryKey: 'id', // Primary key column (default: 'id')
|
|
774
|
+
|
|
775
|
+
relations: {
|
|
776
|
+
// belongsTo: this model has foreign key
|
|
777
|
+
company: {
|
|
778
|
+
type: 'belongsTo',
|
|
779
|
+
model: () => Company,
|
|
780
|
+
foreignKey: 'company_id',
|
|
781
|
+
},
|
|
782
|
+
// hasMany: related model has foreign key
|
|
783
|
+
posts: {
|
|
784
|
+
type: 'hasMany',
|
|
785
|
+
model: () => Post,
|
|
786
|
+
foreignKey: 'user_id',
|
|
787
|
+
},
|
|
788
|
+
// hasOne: like hasMany but returns single record
|
|
789
|
+
profile: {
|
|
790
|
+
type: 'hasOne',
|
|
791
|
+
model: () => Profile,
|
|
792
|
+
foreignKey: 'user_id',
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
|
|
796
|
+
scopes: {
|
|
797
|
+
softDelete: true, // Use deleted_at column
|
|
798
|
+
timestamps: true, // Auto-manage created_at/updated_at
|
|
799
|
+
tenant: 'tenant_id', // Multi-tenant column (optional)
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Repository API
|
|
805
|
+
|
|
806
|
+
```javascript
|
|
807
|
+
const db = createDatabase({ client: 'pg', connection: '...' });
|
|
808
|
+
const UserRepo = db.createRepository(User);
|
|
809
|
+
|
|
810
|
+
// Find by ID (with eager loading)
|
|
811
|
+
const user = await UserRepo.findById(1, { with: ['company', 'posts'] });
|
|
812
|
+
|
|
813
|
+
// Find one by conditions
|
|
814
|
+
const admin = await UserRepo.findOne({ email: 'admin@example.com' });
|
|
815
|
+
|
|
816
|
+
// Find all
|
|
817
|
+
const users = await UserRepo.findAll({ with: ['company'] });
|
|
818
|
+
|
|
819
|
+
// Create
|
|
820
|
+
const newUser = await UserRepo.create({
|
|
821
|
+
email: 'new@example.com',
|
|
822
|
+
name: 'New User',
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// Create many
|
|
826
|
+
const users = await UserRepo.createMany([
|
|
827
|
+
{ email: 'user1@test.com', name: 'User 1' },
|
|
828
|
+
{ email: 'user2@test.com', name: 'User 2' },
|
|
829
|
+
]);
|
|
830
|
+
|
|
831
|
+
// Update
|
|
832
|
+
const updated = await UserRepo.update(1, { name: 'Updated Name' });
|
|
833
|
+
|
|
834
|
+
// Update where
|
|
835
|
+
await UserRepo.updateWhere({ status: 'inactive' }, { status: 'banned' });
|
|
836
|
+
|
|
837
|
+
// Delete (soft delete if enabled)
|
|
838
|
+
await UserRepo.delete(1);
|
|
839
|
+
|
|
840
|
+
// Force delete (permanent)
|
|
841
|
+
await UserRepo.forceDelete(1);
|
|
842
|
+
|
|
843
|
+
// Restore soft-deleted
|
|
844
|
+
await UserRepo.restore(1);
|
|
845
|
+
|
|
846
|
+
// Count
|
|
847
|
+
const count = await UserRepo.count({ status: 'active' });
|
|
848
|
+
|
|
849
|
+
// Exists
|
|
850
|
+
const exists = await UserRepo.exists({ email: 'test@example.com' });
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### Query Builder
|
|
854
|
+
|
|
855
|
+
```javascript
|
|
856
|
+
const users = await UserRepo.query()
|
|
857
|
+
.where({ status: 'active' })
|
|
858
|
+
.where('created_at', '>', '2024-01-01')
|
|
859
|
+
.whereIn('role', ['admin', 'moderator'])
|
|
860
|
+
.whereNotNull('email_verified_at')
|
|
861
|
+
.orderBy('name', 'asc')
|
|
862
|
+
.orderBy('created_at', 'desc')
|
|
863
|
+
.limit(10)
|
|
864
|
+
.offset(20)
|
|
865
|
+
.with('company', 'posts')
|
|
866
|
+
.list();
|
|
867
|
+
|
|
868
|
+
// First result
|
|
869
|
+
const user = await UserRepo.query()
|
|
870
|
+
.where({ email: 'admin@example.com' })
|
|
871
|
+
.first();
|
|
872
|
+
|
|
873
|
+
// Count
|
|
874
|
+
const count = await UserRepo.query()
|
|
875
|
+
.where({ status: 'active' })
|
|
876
|
+
.count();
|
|
877
|
+
|
|
878
|
+
// Pagination
|
|
879
|
+
const result = await UserRepo.query()
|
|
880
|
+
.where({ status: 'active' })
|
|
881
|
+
.orderBy('created_at', 'desc')
|
|
882
|
+
.paginate(1, 20); // page 1, 20 per page
|
|
883
|
+
|
|
884
|
+
// result = { data: [...], total: 150, page: 1, perPage: 20, totalPages: 8 }
|
|
885
|
+
|
|
886
|
+
// Soft delete scopes
|
|
887
|
+
await UserRepo.query().withTrashed().list(); // Include deleted
|
|
888
|
+
await UserRepo.query().onlyTrashed().list(); // Only deleted
|
|
889
|
+
|
|
890
|
+
// Multi-tenant
|
|
891
|
+
await UserRepo.query().forTenant(tenantId).list();
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Transactions
|
|
895
|
+
|
|
896
|
+
```javascript
|
|
897
|
+
await db.transaction(async (trx) => {
|
|
898
|
+
const userRepo = trx.createRepository(User);
|
|
899
|
+
const postRepo = trx.createRepository(Post);
|
|
900
|
+
|
|
901
|
+
const user = await userRepo.create({ email: 'new@test.com', name: 'New' });
|
|
902
|
+
await postRepo.create({ title: 'First Post', user_id: user.id });
|
|
903
|
+
|
|
904
|
+
// All changes committed on success
|
|
905
|
+
// Rolled back on error
|
|
906
|
+
});
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Migrations
|
|
910
|
+
|
|
911
|
+
**CLI Commands:**
|
|
912
|
+
|
|
913
|
+
```bash
|
|
914
|
+
# Run pending migrations
|
|
915
|
+
webspresso db:migrate
|
|
916
|
+
|
|
917
|
+
# Rollback last batch
|
|
918
|
+
webspresso db:rollback
|
|
919
|
+
|
|
920
|
+
# Rollback all
|
|
921
|
+
webspresso db:rollback --all
|
|
922
|
+
|
|
923
|
+
# Show migration status
|
|
924
|
+
webspresso db:status
|
|
925
|
+
|
|
926
|
+
# Create empty migration
|
|
927
|
+
webspresso db:make create_posts_table
|
|
928
|
+
|
|
929
|
+
# Create migration from model (scaffolding)
|
|
930
|
+
webspresso db:make create_users_table --model User
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
**Database Config File (`webspresso.db.js`):**
|
|
934
|
+
|
|
935
|
+
```javascript
|
|
936
|
+
module.exports = {
|
|
937
|
+
client: 'pg', // or 'mysql2', 'better-sqlite3'
|
|
938
|
+
connection: process.env.DATABASE_URL,
|
|
939
|
+
migrations: {
|
|
940
|
+
directory: './migrations',
|
|
941
|
+
tableName: 'knex_migrations',
|
|
942
|
+
},
|
|
943
|
+
|
|
944
|
+
// Environment overrides
|
|
945
|
+
production: {
|
|
946
|
+
connection: process.env.DATABASE_URL,
|
|
947
|
+
pool: { min: 2, max: 10 },
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
**Programmatic API:**
|
|
953
|
+
|
|
954
|
+
```javascript
|
|
955
|
+
const db = createDatabase({
|
|
956
|
+
client: 'pg',
|
|
957
|
+
connection: process.env.DATABASE_URL,
|
|
958
|
+
migrations: { directory: './migrations' },
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
await db.migrate.latest(); // Run pending
|
|
962
|
+
await db.migrate.rollback(); // Rollback last batch
|
|
963
|
+
await db.migrate.rollback({ all: true }); // Rollback all
|
|
964
|
+
const status = await db.migrate.status(); // Get status
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Migration Scaffolding
|
|
968
|
+
|
|
969
|
+
Generate migration from model schema:
|
|
970
|
+
|
|
971
|
+
```javascript
|
|
972
|
+
const { scaffoldMigration } = require('webspresso');
|
|
973
|
+
|
|
974
|
+
const migration = scaffoldMigration(User);
|
|
975
|
+
// Outputs complete migration file content with:
|
|
976
|
+
// - All columns with proper types
|
|
977
|
+
// - Indexes
|
|
978
|
+
// - Foreign key constraints
|
|
979
|
+
// - Up and down functions
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### Supported Databases
|
|
983
|
+
|
|
984
|
+
Install the appropriate driver as a peer dependency:
|
|
985
|
+
|
|
986
|
+
```bash
|
|
987
|
+
# PostgreSQL
|
|
988
|
+
npm install pg
|
|
989
|
+
|
|
990
|
+
# MySQL
|
|
991
|
+
npm install mysql2
|
|
992
|
+
|
|
993
|
+
# SQLite
|
|
994
|
+
npm install better-sqlite3
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
### Design Philosophy
|
|
998
|
+
|
|
999
|
+
| Boundary | Zod's Job | ORM's Job |
|
|
1000
|
+
|----------|-----------|-----------|
|
|
1001
|
+
| Schema definition | Type shape, validation rules | Column metadata extraction |
|
|
1002
|
+
| Input validation | `.parse()` / `.safeParse()` | Never - pass through to Zod |
|
|
1003
|
+
| Query building | N/A | Full ownership |
|
|
1004
|
+
| Relation resolution | N/A | Eager loading with batch queries |
|
|
1005
|
+
| Timestamps/SoftDelete | N/A | Auto-inject on operations |
|
|
1006
|
+
|
|
1007
|
+
**N+1 Prevention:** Relations are always loaded with batch `WHERE IN (...)` queries, never with individual queries per record.
|
|
1008
|
+
|
|
1009
|
+
### Database Seeding
|
|
1010
|
+
|
|
1011
|
+
Generate fake data for testing and development using `@faker-js/faker`:
|
|
1012
|
+
|
|
1013
|
+
```bash
|
|
1014
|
+
npm install @faker-js/faker
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
**Basic Usage:**
|
|
1018
|
+
|
|
1019
|
+
```javascript
|
|
1020
|
+
const { faker } = require('@faker-js/faker');
|
|
1021
|
+
const db = createDatabase({ /* config */ });
|
|
1022
|
+
|
|
1023
|
+
const seeder = db.seeder(faker);
|
|
1024
|
+
|
|
1025
|
+
// Generate a single record
|
|
1026
|
+
const user = await seeder.factory('User').create();
|
|
1027
|
+
|
|
1028
|
+
// Generate multiple records
|
|
1029
|
+
const users = await seeder.factory('User').create(10);
|
|
1030
|
+
|
|
1031
|
+
// Generate without saving (for testing)
|
|
1032
|
+
const userData = seeder.factory('User').make();
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
**Define Factories with Defaults and States:**
|
|
1036
|
+
|
|
1037
|
+
```javascript
|
|
1038
|
+
seeder.defineFactory('User', {
|
|
1039
|
+
// Default values
|
|
1040
|
+
defaults: {
|
|
1041
|
+
status: 'pending',
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
// Custom generators
|
|
1045
|
+
generators: {
|
|
1046
|
+
username: (f) => f.internet.username().toLowerCase(),
|
|
1047
|
+
},
|
|
1048
|
+
|
|
1049
|
+
// Named states for variations
|
|
1050
|
+
states: {
|
|
1051
|
+
admin: { role: 'admin', status: 'active' },
|
|
1052
|
+
verified: (f) => ({
|
|
1053
|
+
status: 'verified',
|
|
1054
|
+
verified_at: f.date.past().toISOString(),
|
|
1055
|
+
}),
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Use states
|
|
1060
|
+
const admin = await seeder.factory('User').state('admin').create();
|
|
1061
|
+
const verified = await seeder.factory('User').state('verified').create();
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
**Smart Field Detection:**
|
|
1065
|
+
|
|
1066
|
+
The seeder automatically generates appropriate fake data based on column names:
|
|
1067
|
+
|
|
1068
|
+
| Field Name Pattern | Generated Data |
|
|
1069
|
+
|-------------------|----------------|
|
|
1070
|
+
| `email`, `*_email` | Valid email address |
|
|
1071
|
+
| `name`, `first_name`, `last_name` | Person names |
|
|
1072
|
+
| `username` | Username |
|
|
1073
|
+
| `title` | Short sentence |
|
|
1074
|
+
| `content`, `body`, `description` | Paragraphs |
|
|
1075
|
+
| `slug` | URL-safe slug |
|
|
1076
|
+
| `phone`, `tel` | Phone number |
|
|
1077
|
+
| `address`, `city`, `country` | Location data |
|
|
1078
|
+
| `price`, `amount`, `cost` | Decimal numbers |
|
|
1079
|
+
| `*_url`, `avatar`, `image` | URLs |
|
|
1080
|
+
|
|
1081
|
+
**Override and Custom Generators:**
|
|
1082
|
+
|
|
1083
|
+
```javascript
|
|
1084
|
+
const user = await seeder.factory('User')
|
|
1085
|
+
.override({ email: 'test@example.com' })
|
|
1086
|
+
.generators({
|
|
1087
|
+
code: (f) => `USR-${f.string.alphanumeric(8)}`,
|
|
1088
|
+
})
|
|
1089
|
+
.create();
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
**Batch Seeding:**
|
|
1093
|
+
|
|
1094
|
+
```javascript
|
|
1095
|
+
// Seed multiple models at once
|
|
1096
|
+
const results = await seeder.run([
|
|
1097
|
+
{ model: 'Company', count: 5 },
|
|
1098
|
+
{ model: 'User', count: 20, state: 'active' },
|
|
1099
|
+
{ model: 'Post', count: 50 },
|
|
1100
|
+
]);
|
|
1101
|
+
|
|
1102
|
+
// Access results
|
|
1103
|
+
console.log(results.Company); // Array of 5 companies
|
|
1104
|
+
console.log(results.User); // Array of 20 users
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**Cleanup:**
|
|
1108
|
+
|
|
1109
|
+
```javascript
|
|
1110
|
+
// Truncate specific tables
|
|
1111
|
+
await seeder.truncate('User');
|
|
1112
|
+
await seeder.truncate(['User', 'Post']);
|
|
1113
|
+
|
|
1114
|
+
// Clear all registered model tables
|
|
1115
|
+
await seeder.clearAll();
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
### Schema Explorer Plugin
|
|
1119
|
+
|
|
1120
|
+
A plugin that exposes ORM schema information via API endpoints. Useful for frontend code generation, documentation, or admin tools.
|
|
1121
|
+
|
|
1122
|
+
**Setup:**
|
|
1123
|
+
|
|
1124
|
+
```javascript
|
|
1125
|
+
const { createApp, schemaExplorerPlugin } = require('webspresso');
|
|
1126
|
+
|
|
1127
|
+
const app = createApp({
|
|
1128
|
+
plugins: [
|
|
1129
|
+
schemaExplorerPlugin({
|
|
1130
|
+
path: '/_schema', // Endpoint path (default: '/_schema')
|
|
1131
|
+
enabled: true, // Force enable (default: auto based on NODE_ENV)
|
|
1132
|
+
exclude: ['Secret'], // Exclude specific models
|
|
1133
|
+
includeColumns: true, // Include column metadata
|
|
1134
|
+
includeRelations: true, // Include relation metadata
|
|
1135
|
+
includeScopes: true, // Include scope configuration
|
|
1136
|
+
authorize: (req) => { // Custom authorization
|
|
1137
|
+
return req.headers['x-api-key'] === 'secret';
|
|
1138
|
+
},
|
|
1139
|
+
}),
|
|
1140
|
+
],
|
|
1141
|
+
});
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
**Endpoints:**
|
|
1145
|
+
|
|
1146
|
+
- `GET /_schema` - List all models
|
|
1147
|
+
- `GET /_schema/:modelName` - Get single model details
|
|
1148
|
+
- `GET /_schema/openapi` - Export in OpenAPI 3.0 schema format
|
|
1149
|
+
|
|
1150
|
+
**Example Response (`GET /_schema`):**
|
|
1151
|
+
|
|
1152
|
+
```json
|
|
1153
|
+
{
|
|
1154
|
+
"meta": {
|
|
1155
|
+
"version": "1.0.0",
|
|
1156
|
+
"generatedAt": "2024-01-01T12:00:00.000Z",
|
|
1157
|
+
"modelCount": 2
|
|
1158
|
+
},
|
|
1159
|
+
"models": [
|
|
1160
|
+
{
|
|
1161
|
+
"name": "User",
|
|
1162
|
+
"table": "users",
|
|
1163
|
+
"primaryKey": "id",
|
|
1164
|
+
"columns": [
|
|
1165
|
+
{ "name": "id", "type": "bigint", "primary": true, "autoIncrement": true },
|
|
1166
|
+
{ "name": "email", "type": "string", "unique": true },
|
|
1167
|
+
{ "name": "company_id", "type": "bigint", "references": "companies" }
|
|
1168
|
+
],
|
|
1169
|
+
"relations": [
|
|
1170
|
+
{ "name": "company", "type": "belongsTo", "relatedModel": "Company", "foreignKey": "company_id" }
|
|
1171
|
+
],
|
|
1172
|
+
"scopes": { "softDelete": true, "timestamps": true, "tenant": null }
|
|
1173
|
+
}
|
|
1174
|
+
]
|
|
1175
|
+
}
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
**Plugin API (programmatic usage):**
|
|
1179
|
+
|
|
1180
|
+
```javascript
|
|
1181
|
+
const plugin = schemaExplorerPlugin();
|
|
1182
|
+
|
|
1183
|
+
// Plugin API can be used by other plugins or in code
|
|
1184
|
+
const models = plugin.api.getModels(); // All models
|
|
1185
|
+
const user = plugin.api.getModel('User'); // Single model
|
|
1186
|
+
const names = plugin.api.getModelNames(); // Model names
|
|
1187
|
+
```
|
|
624
1188
|
|
|
625
1189
|
## Development
|
|
626
1190
|
|