webspresso 0.0.60 → 0.0.61
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 +4 -0
- package/core/openapi/orm-components.js +7 -0
- package/core/orm/index.js +2 -0
- package/core/orm/migrations/scaffold.js +13 -0
- package/core/orm/schema-helpers.js +54 -0
- package/core/orm/scopes.js +27 -0
- package/core/orm/seeder.js +4 -0
- package/core/orm/types.js +1 -1
- package/core/orm/utils/nanoid.js +30 -0
- package/package.json +1 -1
- package/plugins/admin-panel/components.js +1 -0
- package/plugins/admin-panel/field-renderers/index.js +1 -0
- package/templates/skills/webspresso-usage/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -1240,6 +1240,7 @@ The `zdb` helpers wrap Zod schemas with database column metadata:
|
|
|
1240
1240
|
|--------|-------------|---------|
|
|
1241
1241
|
| `zdb.id()` | Primary key (bigint, auto-increment) | |
|
|
1242
1242
|
| `zdb.uuid()` | UUID primary key | |
|
|
1243
|
+
| `zdb.nanoid(opts)` | Nanoid primary key (URL-safe string, stored as VARCHAR) | `maxLength` (default `21`) |
|
|
1243
1244
|
| `zdb.string(opts)` | VARCHAR column | `maxLength`, `unique`, `index`, `nullable` |
|
|
1244
1245
|
| `zdb.text(opts)` | TEXT column | `nullable` |
|
|
1245
1246
|
| `zdb.integer(opts)` | INTEGER column | `nullable`, `default` |
|
|
@@ -1255,6 +1256,9 @@ The `zdb` helpers wrap Zod schemas with database column metadata:
|
|
|
1255
1256
|
| `zdb.enum(values, opts)` | ENUM column | `default`, `nullable` |
|
|
1256
1257
|
| `zdb.foreignKey(table, opts)` | Foreign key (bigint) | `referenceColumn`, `nullable` |
|
|
1257
1258
|
| `zdb.foreignUuid(table, opts)` | Foreign key (uuid) | `referenceColumn`, `nullable` |
|
|
1259
|
+
| `zdb.foreignNanoid(table, opts)` | Foreign key (nanoid string) | `referenceColumn`, `nullable`, `maxLength` (must match referenced PK) |
|
|
1260
|
+
|
|
1261
|
+
**Nanoid columns:** Migration scaffolding emits `table.string(column, maxLength)`. For a **nanoid primary key**, if you omit the primary key on `repository.create()`, Webspresso fills it with a cryptographically random ID using the same default alphabet as the [`nanoid`](https://github.com/ai/nanoid) package (implemented in-framework; no extra dependency). You can also call **`generateNanoid`** (exported from `webspresso`) anywhere you need the same generator.
|
|
1258
1262
|
|
|
1259
1263
|
### Model Definition
|
|
1260
1264
|
|
|
@@ -101,6 +101,13 @@ function columnToOpenApiType(meta) {
|
|
|
101
101
|
result.format = 'uuid';
|
|
102
102
|
break;
|
|
103
103
|
|
|
104
|
+
case 'nanoid':
|
|
105
|
+
result.type = 'string';
|
|
106
|
+
if (meta.maxLength) {
|
|
107
|
+
result.maxLength = meta.maxLength;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
|
|
104
111
|
case 'json':
|
|
105
112
|
result.type = 'object';
|
|
106
113
|
break;
|
package/core/orm/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const { createSeeder } = require('./seeder');
|
|
|
14
14
|
const { createScopeContext } = require('./scopes');
|
|
15
15
|
const { ModelEvents, Hooks, HookCancellationError, createEventContext } = require('./events');
|
|
16
16
|
const { omitHiddenColumns, sanitizeForOutput } = require('./utils');
|
|
17
|
+
const { generateNanoid } = require('./utils/nanoid');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Create a database instance
|
|
@@ -273,6 +274,7 @@ module.exports = {
|
|
|
273
274
|
// Column utilities
|
|
274
275
|
extractColumnsFromSchema,
|
|
275
276
|
getColumnMeta,
|
|
277
|
+
generateNanoid,
|
|
276
278
|
// Output sanitization (exclude hidden columns from API/templates)
|
|
277
279
|
omitHiddenColumns,
|
|
278
280
|
sanitizeForOutput,
|
|
@@ -164,6 +164,19 @@ function generateColumnLine(columnName, meta) {
|
|
|
164
164
|
}
|
|
165
165
|
break;
|
|
166
166
|
|
|
167
|
+
case 'nanoid': {
|
|
168
|
+
const nanoLen = meta.maxLength || 21;
|
|
169
|
+
if (meta.primary) {
|
|
170
|
+
parts.push(`table.string('${columnName}', ${nanoLen})`);
|
|
171
|
+
} else if (meta.references) {
|
|
172
|
+
parts.push(`table.string('${columnName}', ${nanoLen})`);
|
|
173
|
+
fkLine = `table.foreign('${columnName}').references('${meta.referenceColumn || 'id'}').inTable('${meta.references}');`;
|
|
174
|
+
} else {
|
|
175
|
+
parts.push(`table.string('${columnName}', ${nanoLen})`);
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
default:
|
|
168
181
|
parts.push(`table.string('${columnName}')`);
|
|
169
182
|
}
|
|
@@ -359,6 +359,30 @@ function createSchemaHelpers(z) {
|
|
|
359
359
|
}, z);
|
|
360
360
|
},
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Nanoid primary key column (URL-safe string, default length 21)
|
|
364
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
365
|
+
* @returns {SchemaBuilder}
|
|
366
|
+
*/
|
|
367
|
+
nanoid(options = {}) {
|
|
368
|
+
const { maxLength = 21, ...rest } = options;
|
|
369
|
+
const schema = z
|
|
370
|
+
.string()
|
|
371
|
+
.length(maxLength)
|
|
372
|
+
.regex(/^[A-Za-z0-9_-]+$/)
|
|
373
|
+
.optional();
|
|
374
|
+
return createSchemaBuilder(
|
|
375
|
+
schema,
|
|
376
|
+
{
|
|
377
|
+
type: 'nanoid',
|
|
378
|
+
primary: true,
|
|
379
|
+
maxLength,
|
|
380
|
+
...rest,
|
|
381
|
+
},
|
|
382
|
+
z
|
|
383
|
+
);
|
|
384
|
+
},
|
|
385
|
+
|
|
362
386
|
/**
|
|
363
387
|
* String column (varchar)
|
|
364
388
|
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
@@ -667,6 +691,36 @@ function createSchemaHelpers(z) {
|
|
|
667
691
|
...rest,
|
|
668
692
|
}, z);
|
|
669
693
|
},
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Nanoid foreign key column
|
|
697
|
+
* @param {string} references - Referenced table name
|
|
698
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
699
|
+
* @returns {SchemaBuilder}
|
|
700
|
+
*/
|
|
701
|
+
foreignNanoid(references, options = {}) {
|
|
702
|
+
const {
|
|
703
|
+
referenceColumn = 'id',
|
|
704
|
+
nullable = false,
|
|
705
|
+
maxLength = 21,
|
|
706
|
+
...rest
|
|
707
|
+
} = options;
|
|
708
|
+
let schema = z
|
|
709
|
+
.string()
|
|
710
|
+
.length(maxLength)
|
|
711
|
+
.regex(/^[A-Za-z0-9_-]+$/);
|
|
712
|
+
if (nullable) {
|
|
713
|
+
schema = schema.nullable().optional();
|
|
714
|
+
}
|
|
715
|
+
return createSchemaBuilder(schema, {
|
|
716
|
+
type: 'nanoid',
|
|
717
|
+
references,
|
|
718
|
+
referenceColumn,
|
|
719
|
+
nullable,
|
|
720
|
+
maxLength,
|
|
721
|
+
...rest,
|
|
722
|
+
}, z);
|
|
723
|
+
},
|
|
670
724
|
};
|
|
671
725
|
|
|
672
726
|
return helpers;
|
package/core/orm/scopes.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @module core/orm/scopes
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const { generateNanoid } = require('./utils/nanoid');
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Apply soft delete scope to a query builder
|
|
9
11
|
* @param {import('knex').Knex.QueryBuilder} qb - Knex query builder
|
|
@@ -115,6 +117,29 @@ function applyInsertTenant(data, context, model) {
|
|
|
115
117
|
};
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Generate nanoid primary key when missing on insert
|
|
122
|
+
* @param {Object} data - Data to insert
|
|
123
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
124
|
+
* @returns {Object}
|
|
125
|
+
*/
|
|
126
|
+
function applyInsertNanoidPrimary(data, model) {
|
|
127
|
+
const pk = model.primaryKey;
|
|
128
|
+
const meta = model.columns && model.columns.get(pk);
|
|
129
|
+
if (!meta || meta.type !== 'nanoid') {
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
const val = data[pk];
|
|
133
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
const len = meta.maxLength || 21;
|
|
137
|
+
return {
|
|
138
|
+
...data,
|
|
139
|
+
[pk]: generateNanoid(len),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
118
143
|
/**
|
|
119
144
|
* Get soft delete data (for UPDATE instead of DELETE)
|
|
120
145
|
* @returns {Object} Soft delete update data
|
|
@@ -142,6 +167,7 @@ function applyInsertModifiers(data, context, model) {
|
|
|
142
167
|
let modified = { ...data };
|
|
143
168
|
modified = applyInsertTimestamps(modified, model);
|
|
144
169
|
modified = applyInsertTenant(modified, context, model);
|
|
170
|
+
modified = applyInsertNanoidPrimary(modified, model);
|
|
145
171
|
return modified;
|
|
146
172
|
}
|
|
147
173
|
|
|
@@ -174,6 +200,7 @@ module.exports = {
|
|
|
174
200
|
applyInsertTimestamps,
|
|
175
201
|
applyUpdateTimestamps,
|
|
176
202
|
applyInsertTenant,
|
|
203
|
+
applyInsertNanoidPrimary,
|
|
177
204
|
applyInsertModifiers,
|
|
178
205
|
applyUpdateModifiers,
|
|
179
206
|
getSoftDeleteData,
|
package/core/orm/seeder.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { getModel, getAllModels } = require('./model');
|
|
8
|
+
const { generateNanoid } = require('./utils/nanoid');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {Object} SeederOptions
|
|
@@ -189,6 +190,9 @@ function createSeeder(faker, knex) {
|
|
|
189
190
|
case 'uuid':
|
|
190
191
|
return faker.string.uuid();
|
|
191
192
|
|
|
193
|
+
case 'nanoid':
|
|
194
|
+
return generateNanoid(meta.maxLength || 21);
|
|
195
|
+
|
|
192
196
|
case 'json':
|
|
193
197
|
return { key: faker.lorem.word(), value: faker.lorem.sentence() };
|
|
194
198
|
|
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'|'array'|'enum'|'uuid'} ColumnType
|
|
12
|
+
* @typedef {'id'|'string'|'text'|'integer'|'bigint'|'float'|'decimal'|'boolean'|'date'|'datetime'|'timestamp'|'json'|'array'|'enum'|'uuid'|'nanoid'} ColumnType
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL-safe ID generation compatible with the default nanoid alphabet/algorithm.
|
|
3
|
+
* @module core/orm/utils/nanoid
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
/** Same 64-char alphabet as npm `nanoid` default (URL-safe). */
|
|
9
|
+
const URL_ALPHABET =
|
|
10
|
+
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {number} [size=21] - Id length (default matches nanoid)
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function generateNanoid(size = 21) {
|
|
17
|
+
const n = Math.max(1, Math.floor(size));
|
|
18
|
+
const bytes = new Uint8Array(n);
|
|
19
|
+
crypto.randomFillSync(bytes);
|
|
20
|
+
let id = '';
|
|
21
|
+
for (let i = 0; i < n; i++) {
|
|
22
|
+
id += URL_ALPHABET[bytes[i] & 63];
|
|
23
|
+
}
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
generateNanoid,
|
|
29
|
+
URL_ALPHABET,
|
|
30
|
+
};
|
package/package.json
CHANGED
|
@@ -68,6 +68,7 @@ function initializeDefaultRenderers() {
|
|
|
68
68
|
registerFieldRenderer('timestamp', basicRenderers.DateTimeField);
|
|
69
69
|
registerFieldRenderer('enum', basicRenderers.SelectField);
|
|
70
70
|
registerFieldRenderer('uuid', basicRenderers.TextField);
|
|
71
|
+
registerFieldRenderer('nanoid', basicRenderers.TextField);
|
|
71
72
|
registerFieldRenderer('id', basicRenderers.NumberField);
|
|
72
73
|
|
|
73
74
|
// Complex types
|
|
@@ -143,13 +143,14 @@ Analytics plugin adds `fsy.analyticsHead`, `fsy.verificationTags`, etc., when co
|
|
|
143
143
|
|
|
144
144
|
## 9. ORM overview
|
|
145
145
|
|
|
146
|
-
**Define schema** with **`zdb`** (`zdb.id()`, `zdb.string({...})`, `zdb.foreignKey`, `zdb.timestamp`, `zdb.json`, …).
|
|
146
|
+
**Define schema** with **`zdb`** (`zdb.id()`, `zdb.uuid()`, `zdb.nanoid()`, `zdb.string({...})`, `zdb.foreignKey`, `zdb.foreignUuid`, `zdb.foreignNanoid`, `zdb.timestamp`, `zdb.json`, …).
|
|
147
147
|
|
|
148
148
|
**Define model** with **`defineModel({ name, table, schema, relations, scopes, hidden, admin })`**.
|
|
149
149
|
|
|
150
150
|
- **Relations:** `belongsTo`, `hasMany`, `hasOne` with `model: () => OtherModel`.
|
|
151
151
|
- **Scopes:** `softDelete`, `timestamps`, optional `tenant` column.
|
|
152
152
|
- **`hidden`:** columns never exposed in admin/API (e.g. `password_hash`).
|
|
153
|
+
- **Nanoid PK:** `zdb.nanoid()` / `zdb.nanoid({ maxLength: 12 })` — string primary key; migrations use `string(length)`. On **`create()`**, omitting the PK auto-fills a URL-safe id (built-in generator, same alphabet as `nanoid`). Use **`zdb.foreignNanoid('table', { maxLength })`** when the parent uses nanoid PKs; **`generateNanoid`** is exported from `webspresso` for manual ids.
|
|
153
154
|
|
|
154
155
|
**Database:** `createDatabase({ client, connection, models: './models' })` — auto-loads `models/*.js` (ignore `_prefix`).
|
|
155
156
|
|