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