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 CHANGED
@@ -49,13 +49,19 @@ npm run dev
49
49
 
50
50
  ## CLI Commands
51
51
 
52
- ### `webspresso new <project-name>`
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