webspresso 0.0.13 → 0.0.14

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.
Files changed (41) hide show
  1. package/README.md +4 -7
  2. package/bin/commands/add-tailwind.js +151 -0
  3. package/bin/commands/api.js +70 -0
  4. package/bin/commands/db-make.js +76 -0
  5. package/bin/commands/db-migrate.js +43 -0
  6. package/bin/commands/db-rollback.js +48 -0
  7. package/bin/commands/db-status.js +53 -0
  8. package/bin/commands/dev.js +73 -0
  9. package/bin/commands/new.js +634 -0
  10. package/bin/commands/page.js +134 -0
  11. package/bin/commands/seed.js +154 -0
  12. package/bin/commands/start.js +30 -0
  13. package/bin/utils/db.js +54 -0
  14. package/bin/utils/migration.js +36 -0
  15. package/bin/utils/project.js +97 -0
  16. package/bin/utils/seed.js +112 -0
  17. package/bin/webspresso.js +24 -1696
  18. package/core/orm/index.js +14 -1
  19. package/core/orm/migrations/scaffold.js +5 -0
  20. package/core/orm/model.js +8 -0
  21. package/core/orm/schema-helpers.js +39 -1
  22. package/core/orm/seeder.js +56 -3
  23. package/core/orm/types.js +28 -1
  24. package/index.js +2 -1
  25. package/package.json +1 -1
  26. package/plugins/admin-panel/admin-user-model.js +42 -0
  27. package/plugins/admin-panel/api.js +436 -0
  28. package/plugins/admin-panel/app.js +68 -0
  29. package/plugins/admin-panel/auth.js +157 -0
  30. package/plugins/admin-panel/components.js +359 -0
  31. package/plugins/admin-panel/field-renderers/array.js +57 -0
  32. package/plugins/admin-panel/field-renderers/basic.js +205 -0
  33. package/plugins/admin-panel/field-renderers/file-upload.js +124 -0
  34. package/plugins/admin-panel/field-renderers/index.js +93 -0
  35. package/plugins/admin-panel/field-renderers/json.js +52 -0
  36. package/plugins/admin-panel/field-renderers/relations.js +96 -0
  37. package/plugins/admin-panel/field-renderers/rich-text.js +83 -0
  38. package/plugins/admin-panel/index.js +187 -0
  39. package/plugins/admin-panel/migration-template.js +39 -0
  40. package/plugins/admin-panel/styles.js +9 -0
  41. package/plugins/index.js +2 -0
package/core/orm/index.js CHANGED
@@ -6,6 +6,18 @@
6
6
 
7
7
  const { createSchemaHelpers, extractColumnsFromSchema, getColumnMeta } = require('./schema-helpers');
8
8
  const { defineModel, getModel, getAllModels, hasModel, clearRegistry } = require('./model');
9
+
10
+ // Create zdb instance with zod (zod is a dependency)
11
+ let z;
12
+ try {
13
+ z = require('zod');
14
+ } catch {
15
+ // Zod not installed, zdb will be undefined
16
+ z = null;
17
+ }
18
+
19
+ // Export zdb instance directly
20
+ const zdb = z ? createSchemaHelpers(z) : null;
9
21
  const { createRepository } = require('./repository');
10
22
  const { createQueryBuilder, QueryBuilder } = require('./query-builder');
11
23
  const { runTransaction, createTransactionContext } = require('./transaction');
@@ -110,7 +122,8 @@ module.exports = {
110
122
  // Main factory
111
123
  createDatabase,
112
124
 
113
- // Schema helpers
125
+ // Schema helpers - zdb instance (direct export)
126
+ zdb,
114
127
  createSchemaHelpers,
115
128
  extractColumnsFromSchema,
116
129
  getColumnMeta,
@@ -142,6 +142,11 @@ function generateColumnLine(columnName, meta) {
142
142
  parts.push(`table.json('${columnName}')`);
143
143
  break;
144
144
 
145
+ case 'array':
146
+ // Array is stored as JSON in database
147
+ parts.push(`table.json('${columnName}')`);
148
+ break;
149
+
145
150
  case 'enum':
146
151
  const enumValues = meta.enumValues || [];
147
152
  const valuesStr = enumValues.map(v => `'${v}'`).join(', ');
package/core/orm/model.js CHANGED
@@ -25,6 +25,7 @@ function defineModel(options) {
25
25
  primaryKey = 'id',
26
26
  relations = {},
27
27
  scopes = {},
28
+ admin = {},
28
29
  } = options;
29
30
 
30
31
  // Validate required fields
@@ -78,6 +79,13 @@ function defineModel(options) {
78
79
  tenant: scopes.tenant || null,
79
80
  },
80
81
  columns,
82
+ admin: {
83
+ enabled: admin.enabled || false,
84
+ label: admin.label || name,
85
+ icon: admin.icon || null,
86
+ customFields: admin.customFields || {},
87
+ queries: admin.queries || {},
88
+ },
81
89
  };
82
90
 
83
91
  // Register model
@@ -67,7 +67,15 @@ function createSchemaHelpers(z) {
67
67
  return schema.describe(encodeColumnMeta(meta));
68
68
  }
69
69
 
70
- return {
70
+ const helpers = {
71
+ /**
72
+ * Create a Zod object schema with database metadata
73
+ * @param {Object} shape - Object shape with zdb fields
74
+ * @returns {import('zod').ZodObject}
75
+ */
76
+ schema(shape) {
77
+ return z.object(shape);
78
+ },
71
79
  /**
72
80
  * Primary key column (bigint, auto-increment)
73
81
  * @param {Partial<import('./types').ColumnMeta>} [options={}]
@@ -304,6 +312,34 @@ function createSchemaHelpers(z) {
304
312
  });
305
313
  },
306
314
 
315
+ /**
316
+ * Array column (stored as JSON in database)
317
+ * @param {import('zod').ZodTypeAny} [itemSchema] - Schema for array items (default: z.any())
318
+ * @param {Partial<import('./types').ColumnMeta>} [options={}]
319
+ * @returns {import('zod').ZodArray}
320
+ */
321
+ array(itemSchema, options = {}) {
322
+ // If first argument is options object (backward compatibility)
323
+ if (itemSchema && typeof itemSchema === 'object' && !itemSchema._def) {
324
+ options = itemSchema;
325
+ itemSchema = z.any();
326
+ }
327
+
328
+ const { nullable = false, ...rest } = options;
329
+ const baseItemSchema = itemSchema || z.any();
330
+ let schema = z.array(baseItemSchema);
331
+
332
+ if (nullable) {
333
+ schema = schema.nullable().optional();
334
+ }
335
+
336
+ return withMeta(schema, {
337
+ type: 'array',
338
+ nullable,
339
+ ...rest,
340
+ });
341
+ },
342
+
307
343
  /**
308
344
  * Enum column
309
345
  * @param {string[]} values - Allowed enum values
@@ -370,6 +406,8 @@ function createSchemaHelpers(z) {
370
406
  });
371
407
  },
372
408
  };
409
+
410
+ return helpers;
373
411
  }
374
412
 
375
413
  /**
@@ -61,6 +61,11 @@ function createSeeder(faker, knex) {
61
61
  // Smart field detection by name
62
62
  const lowerName = columnName.toLowerCase();
63
63
 
64
+ // Array type should be handled in generateByType (skip smart detection for arrays)
65
+ if (meta.type === 'array') {
66
+ return generateByType(columnName, meta);
67
+ }
68
+
64
69
  // Email detection
65
70
  if (lowerName.includes('email')) {
66
71
  return faker.internet.email().toLowerCase();
@@ -146,16 +151,17 @@ function createSeeder(faker, knex) {
146
151
  return faker.helpers.arrayElement(['active', 'inactive', 'pending']);
147
152
  }
148
153
 
149
- // Generate by type
150
- return generateByType(meta);
154
+ // Generate by type (pass columnName for array type detection)
155
+ return generateByType(columnName, meta);
151
156
  }
152
157
 
153
158
  /**
154
159
  * Generate value by column type
160
+ * @param {string} columnName - Column name (for smart detection, especially for arrays)
155
161
  * @param {Object} meta - Column metadata
156
162
  * @returns {*} Generated value
157
163
  */
158
- function generateByType(meta) {
164
+ function generateByType(columnName, meta) {
159
165
  switch (meta.type) {
160
166
  case 'bigint':
161
167
  case 'integer':
@@ -186,6 +192,53 @@ function createSeeder(faker, knex) {
186
192
  case 'json':
187
193
  return { key: faker.lorem.word(), value: faker.lorem.sentence() };
188
194
 
195
+ case 'array':
196
+ // Generate a random array with 1-5 items
197
+ const arrayLength = faker.number.int({ min: 1, max: 5 });
198
+ const arrayItems = [];
199
+
200
+ // Try to infer item type from column name
201
+ const lowerName = (columnName || '').toLowerCase();
202
+
203
+ // String arrays (tags, categories, etc.)
204
+ if (lowerName.includes('tag') || lowerName.includes('category') ||
205
+ lowerName.includes('label') || lowerName.includes('keyword')) {
206
+ for (let i = 0; i < arrayLength; i++) {
207
+ arrayItems.push(faker.lorem.word());
208
+ }
209
+ }
210
+ // Number arrays (ids, scores, etc.)
211
+ else if (lowerName.includes('id') || lowerName.includes('score') ||
212
+ lowerName.includes('price') || lowerName.includes('amount')) {
213
+ for (let i = 0; i < arrayLength; i++) {
214
+ arrayItems.push(faker.number.int({ min: 1, max: 100 }));
215
+ }
216
+ }
217
+ // Email arrays
218
+ else if (lowerName.includes('email')) {
219
+ for (let i = 0; i < arrayLength; i++) {
220
+ arrayItems.push(faker.internet.email().toLowerCase());
221
+ }
222
+ }
223
+ // URL arrays
224
+ else if (lowerName.includes('url') || lowerName.includes('link')) {
225
+ for (let i = 0; i < arrayLength; i++) {
226
+ arrayItems.push(faker.internet.url());
227
+ }
228
+ }
229
+ // Default: mixed array (strings and numbers)
230
+ else {
231
+ for (let i = 0; i < arrayLength; i++) {
232
+ if (faker.datatype.boolean()) {
233
+ arrayItems.push(faker.lorem.word());
234
+ } else {
235
+ arrayItems.push(faker.number.int({ min: 1, max: 100 }));
236
+ }
237
+ }
238
+ }
239
+
240
+ return arrayItems;
241
+
189
242
  case 'enum':
190
243
  if (meta.enumValues && meta.enumValues.length > 0) {
191
244
  return faker.helpers.arrayElement(meta.enumValues);
package/core/orm/types.js CHANGED
@@ -9,7 +9,7 @@
9
9
  // ============================================================================
10
10
 
11
11
  /**
12
- * @typedef {'id'|'string'|'text'|'integer'|'bigint'|'float'|'decimal'|'boolean'|'date'|'datetime'|'timestamp'|'json'|'enum'|'uuid'} ColumnType
12
+ * @typedef {'id'|'string'|'text'|'integer'|'bigint'|'float'|'decimal'|'boolean'|'date'|'datetime'|'timestamp'|'json'|'array'|'enum'|'uuid'} ColumnType
13
13
  */
14
14
 
15
15
  /**
@@ -75,6 +75,31 @@
75
75
  * @property {boolean} [onlyTrashed=false] - Only soft-deleted records
76
76
  */
77
77
 
78
+ // ============================================================================
79
+ // Admin Panel Types
80
+ // ============================================================================
81
+
82
+ /**
83
+ * @typedef {Object} CustomFieldConfig
84
+ * @property {string} type - Field renderer type (e.g., 'file-upload', 'rich-text')
85
+ * @property {*} [options] - Additional options for the field renderer
86
+ */
87
+
88
+ /**
89
+ * @typedef {Function} QueryConfig
90
+ * @param {import('./repository').Repository} repo - Repository instance
91
+ * @returns {Promise<*>} Query result
92
+ */
93
+
94
+ /**
95
+ * @typedef {Object} AdminMetadata
96
+ * @property {boolean} [enabled=false] - Whether model is enabled in admin panel
97
+ * @property {string} [label] - Display label for the model (default: model name)
98
+ * @property {string} [icon] - Icon for the model (emoji or icon name)
99
+ * @property {Object.<string, CustomFieldConfig>} [customFields={}] - Custom field configurations
100
+ * @property {Object.<string, QueryConfig>} [queries={}] - Custom query functions
101
+ */
102
+
78
103
  // ============================================================================
79
104
  // Model Types
80
105
  // ============================================================================
@@ -87,6 +112,7 @@
87
112
  * @property {string} [primaryKey='id'] - Primary key column name
88
113
  * @property {RelationsMap} [relations={}] - Relation definitions
89
114
  * @property {ScopeOptions} [scopes={}] - Scope options
115
+ * @property {AdminMetadata} [admin] - Admin panel metadata
90
116
  */
91
117
 
92
118
  /**
@@ -98,6 +124,7 @@
98
124
  * @property {RelationsMap} relations - Relation definitions
99
125
  * @property {ScopeOptions} scopes - Scope options
100
126
  * @property {Map<string, ColumnMeta>} columns - Parsed column metadata
127
+ * @property {AdminMetadata} [admin] - Admin panel metadata
101
128
  */
102
129
 
103
130
  // ============================================================================
package/index.js CHANGED
@@ -30,7 +30,7 @@ const {
30
30
  const orm = require('./core/orm');
31
31
 
32
32
  // Built-in plugins
33
- const { schemaExplorerPlugin } = require('./plugins');
33
+ const { schemaExplorerPlugin, adminPanelPlugin } = require('./plugins');
34
34
 
35
35
  module.exports = {
36
36
  // Main API
@@ -65,5 +65,6 @@ module.exports = {
65
65
 
66
66
  // Plugins
67
67
  schemaExplorerPlugin,
68
+ adminPanelPlugin,
68
69
  };
69
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Admin User Model
3
+ * Model definition for admin panel users
4
+ * @module plugins/admin-panel/admin-user-model
5
+ */
6
+
7
+ const { defineModel } = require('../../../core/orm/model');
8
+ const { zdb } = require('../../../core/orm');
9
+
10
+ /**
11
+ * AdminUser model schema
12
+ */
13
+ const AdminUserSchema = zdb.schema({
14
+ id: zdb.id(),
15
+ email: zdb.string({ unique: true, maxLength: 255 }),
16
+ password: zdb.string({ maxLength: 255 }), // Hashed password
17
+ name: zdb.string({ maxLength: 255 }),
18
+ role: zdb.string({ maxLength: 50, default: 'admin' }),
19
+ active: zdb.boolean({ default: true }),
20
+ created_at: zdb.timestamp({ auto: 'create' }),
21
+ updated_at: zdb.timestamp({ auto: 'update' }),
22
+ });
23
+
24
+ /**
25
+ * Create and register AdminUser model
26
+ * @returns {import('../../../core/orm/types').ModelDefinition}
27
+ */
28
+ function createAdminUserModel() {
29
+ return defineModel({
30
+ name: 'AdminUser',
31
+ table: 'admin_users',
32
+ schema: AdminUserSchema,
33
+ scopes: {
34
+ timestamps: true,
35
+ },
36
+ });
37
+ }
38
+
39
+ module.exports = {
40
+ createAdminUserModel,
41
+ AdminUserSchema,
42
+ };