cabloy 5.1.156 → 5.1.158

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/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.156
1
+ 5.1.158
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.158
4
+
5
+ ### Improvements
6
+
7
+ - Add licensing documentation.
8
+ - Add contribution guidelines.
9
+
10
+ ## 5.1.157
11
+
12
+ ### Features
13
+
14
+ - Add `coerceColumn` support.
15
+
3
16
  ## 5.1.156
4
17
 
5
18
  ### Features
@@ -0,0 +1,84 @@
1
+ # Contributing to Cabloy Basic
2
+
3
+ Contributions to Cabloy Basic source, tests, tooling, and documentation are welcome. Cabloy Basic is the public Cabloy framework/reference edition, with Vona backend and Zova frontend layers.
4
+
5
+ ## Before you begin
6
+
7
+ - Use [GitHub Issues](https://github.com/cabloy/cabloy/issues) for reproducible bugs, public questions, and feature proposals. Search for an existing issue first.
8
+ - Use [pull requests](https://github.com/cabloy/cabloy/pulls) for focused, reviewable changes.
9
+ - For a bug report, include the expected and actual behavior, a minimal reproduction or sufficient setup details, and relevant Cabloy, Node.js, pnpm, database, and browser/runtime versions.
10
+ - Remove credentials, tokens, personal data, and sensitive system information from issues, logs, screenshots, and pull requests.
11
+
12
+ ## Set up a development checkout
13
+
14
+ You need Git, Node.js `>=24.4.0`, pnpm `>=11.5.2`, Redis `>=7.2.6`, and one documented development database baseline: SQLite, MySQL `>=8`, or PostgreSQL `>=16`. SQLite setups using `better-sqlite3` may also require a native build toolchain for `node-gyp`.
15
+
16
+ ```bash
17
+ git clone https://github.com/cabloy/cabloy.git
18
+ cd cabloy
19
+ npm run init
20
+ ```
21
+
22
+ `npm run init` is more than a package install: it installs multiple workspaces, prepares generated local configuration, initializes and synchronizes Vona and Zova, and generates required frontend and backend artifacts. Run it intentionally rather than as a retry for an unrelated narrow change.
23
+
24
+ Current CI workflows run on Ubuntu with Node.js 24 after pushes to `main`; do not assume that every pull request is automatically validated. For maintained onboarding guidance, see [Cabloy Basic](https://cabloy.com/editions/cabloy-basic).
25
+
26
+ ## Develop and validate
27
+
28
+ Start with the narrowest meaningful check, then broaden validation when a change crosses a shared boundary.
29
+
30
+ | Purpose | Command |
31
+ | ---------------------------------------- | -------------------------- |
32
+ | Start Vona development | `npm run dev` |
33
+ | Start one Vona development process | `npm run dev:one` |
34
+ | Start Basic Admin SSR development | `npm run dev:zova:admin` |
35
+ | Start Basic Web SSR development | `npm run dev:zova:web` |
36
+ | Check formatting | `npm run format` |
37
+ | Lint | `npm run lint` |
38
+ | Type-check Vona and Zova | `npm run tsc` |
39
+ | Run backend tests | `npm run test` |
40
+ | Run the full E2E suite | `npm run test:e2e` |
41
+ | Rerun E2E tests without reset | `npm run test:e2e:fast` |
42
+ | Build required Basic artifacts | `npm run build` |
43
+ | Build Basic Admin SSR and REST artifacts | `npm run build:zova:admin` |
44
+ | Build Basic Web SSR and REST artifacts | `npm run build:zova:web` |
45
+ | Synchronize Zova dependencies into Vona | `npm run deps:vona` |
46
+ | Build the public documentation site | `npm run docs:build` |
47
+
48
+ The tracked pre-commit hook runs `lint-staged`. It may format staged Markdown and configuration files, and may lint-fix and format staged source files. Review the resulting staged changes and run the relevant explicit checks before submitting.
49
+
50
+ ## Generated code and Vona–Zova integration
51
+
52
+ Use Vona backend contracts, DTOs, OpenAPI metadata, and schema metadata as contract truth. Regenerate consumers rather than hand-editing generated frontend artifacts.
53
+
54
+ For frontend metadata or dependency changes that Vona consumes, build every affected Basic flavor before synchronizing dependencies:
55
+
56
+ ```bash
57
+ npm run build:zova:admin
58
+ # Also run npm run build:zova:web when Web is affected.
59
+ npm run deps:vona
60
+ ```
61
+
62
+ Keep SSR and REST outputs aligned; a REST-only build is not sufficient. See the canonical [Contract Loop Playbook](https://cabloy.com/fullstack/contract-loop-playbook), [Vona + Zova Integration](https://cabloy.com/fullstack/vona-zova-integration), and [repository scripts reference](https://cabloy.com/reference/repo-scripts).
63
+
64
+ ## Pull requests
65
+
66
+ Keep each pull request focused and describe its user-visible and technical effects. Include:
67
+
68
+ - the related issue when one exists;
69
+ - tests added or updated for behavioral changes;
70
+ - the exact validation commands run and their results;
71
+ - any regenerated contract artifacts and the build/synchronization steps used; and
72
+ - documentation updates, or why no documentation update is needed.
73
+
74
+ Do not include unrelated generated output, build artifacts, environment files, credentials, or cache files.
75
+
76
+ ## Documentation
77
+
78
+ Canonical public Cabloy documentation is maintained in this repository's `repo-docs/` tree and published at [cabloy.com](https://cabloy.com). Discuss and submit proposed public-documentation changes in an issue or pull request; do not treat copies outside this repository as independent canonical sources.
79
+
80
+ ## License and contribution rights
81
+
82
+ Cabloy Basic project-owned content is licensed under the root [MIT License](./LICENSE).
83
+
84
+ Submit only material that you have the right to contribute. Preserve required copyright, license, attribution, and third-party notices for any included code, media, fonts, generated material, or copied snippets.
package/LICENSES.md ADDED
@@ -0,0 +1,5 @@
1
+ # License Scope
2
+
3
+ Unless a file or directory includes its own complete license or third-party notice, Cabloy Basic project-owned source, documentation, and assets inherit the root [MIT License](./LICENSE).
4
+
5
+ Third-party material remains subject to the license and notice provided with that material or otherwise required by its upstream terms.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.156",
3
+ "version": "5.1.158",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.142",
3
+ "version": "1.1.143",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.140",
3
+ "version": "1.1.141",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cascade-extend",
3
- "version": "2.2.8",
3
+ "version": "2.2.9",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "cascade-extend",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabloy/dotenv",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "cabloy dotenv",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.90",
3
+ "version": "5.1.91",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -45,11 +45,13 @@ describe('role.test.ts', { concurrency: false }, () => {
45
45
  it('bean:role emits only actual membership changes', async () => {
46
46
  let userId: string | undefined;
47
47
  let roleId: string | undefined;
48
- const events: unknown[] = [];
48
+ const events: Array<{ userIds: unknown[]; roleIds: unknown[] }> = [];
49
49
  const event = app.scope('a-user').event.roleMembershipChanged;
50
50
  const originalEmit = event.emit.bind(event);
51
51
  const emitMock = mock.method(event, 'emit', async data => {
52
- events.push(data);
52
+ if (data.userIds.some(item => String(item) === userId)) {
53
+ events.push(data);
54
+ }
53
55
  return await originalEmit(data);
54
56
  });
55
57
  try {
@@ -143,11 +145,13 @@ describe('role.test.ts', { concurrency: false }, () => {
143
145
  it('bean:role replaces a membership set with one change event', async () => {
144
146
  let userId: string | undefined;
145
147
  const roleIds: string[] = [];
146
- const events: unknown[] = [];
148
+ const events: Array<{ userIds: unknown[]; roleIds: unknown[] }> = [];
147
149
  const event = app.scope('a-user').event.roleMembershipChanged;
148
150
  const originalEmit = event.emit.bind(event);
149
151
  const emitMock = mock.method(event, 'emit', async data => {
150
- events.push(data);
152
+ if (data.userIds.some(item => String(item) === userId)) {
153
+ events.push(data);
154
+ }
151
155
  return await originalEmit(data);
152
156
  });
153
157
  try {
@@ -175,7 +179,9 @@ describe('role.test.ts', { concurrency: false }, () => {
175
179
 
176
180
  const result = await app.bean.role.replaceUserRoleIds(user.id, [roles[1].id, roles[2].id]);
177
181
  assert.deepEqual(result, { addedRoleIds: [roles[2].id], removedRoleIds: [roles[0].id] });
178
- assert.deepEqual(events, [{ userIds: [user.id], roleIds: [roles[0].id, roles[2].id] }]);
182
+ assert.equal(events.length, 1);
183
+ assert.deepEqual(events[0]?.userIds, [user.id]);
184
+ assert.deepEqual(events[0]?.roleIds.toSorted(), [roles[0].id, roles[2].id].toSorted());
179
185
 
180
186
  const memberships = await homeUser.model.roleUser.select({ where: { userId: user.id } });
181
187
  assert.deepEqual(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-rbac",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-cabloy",
3
- "version": "5.1.49",
3
+ "version": "5.1.50",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -9,7 +9,7 @@
9
9
  "dependencies": {
10
10
  "vona-module-a-cabloy": "workspace:^",
11
11
  "vona-module-a-markdown": "^5.0.3",
12
- "vona-module-a-rbac": "^5.0.3",
12
+ "vona-module-a-rbac": "^5.0.4",
13
13
  "vona-module-a-socket": "workspace:^",
14
14
  "vona-module-a-ssr": "workspace:^",
15
15
  "vona-module-a-ssrhmr": "workspace:^"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-orm",
3
- "version": "5.1.30",
3
+ "version": "5.1.31",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "db",
6
6
  "keywords": [
@@ -15,17 +15,17 @@ import type {
15
15
  } from '../types/dialect.ts';
16
16
 
17
17
  const BOOLEAN_COLUMN_TYPES = ['bit', 'bool', 'boolean'];
18
- const NUMBER_COLUMN_TYPES = ['int'];
19
- const TIMESTAMP_COLUMN_TYPE_PREFIXES = ['timestamp'];
20
- const FLOAT_COLUMN_TYPE_PREFIXES = ['float', 'double'];
21
- const INTEGER_COLUMN_TYPE_PREFIXES = [
22
- 'tinyint',
23
- 'smallint',
24
- 'mediumint',
25
- 'bigint',
26
- 'numeric',
27
- 'integer',
28
- ];
18
+ const INTEGER_COLUMN_TYPES = ['tinyint', 'smallint', 'mediumint', 'int', 'integer'];
19
+ const FLOAT_COLUMN_TYPES = ['float', 'double'];
20
+ const GENERATED_DEFAULT_PATTERN =
21
+ /^(?:current_timestamp\b(?:\s*\(\s*\d*\s*\))?|current_date\b|current_time\b(?:\s*\(\s*\d*\s*\))?|now\s*\(\s*\)|nextval\s*\()/i;
22
+ const NUMBER_LITERAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
23
+ const QUOTED_LITERAL_PATTERN = /^'((?:''|[^'])*)'$/;
24
+
25
+ type ColumnDefaultLiteral =
26
+ | { kind: 'string'; value: string }
27
+ | { kind: 'boolean'; value: boolean }
28
+ | { kind: 'number'; value: string };
29
29
 
30
30
  @Service()
31
31
  @Virtual()
@@ -96,13 +96,13 @@ export class ServiceDatabaseDialectBase extends BeanBase {
96
96
 
97
97
  coerceColumn(column: Knex.ColumnInfo): ITableColumn {
98
98
  const result = { type: column.type } as ITableColumn;
99
- result.default = this._coerceColumnValue(column.type, column.defaultValue);
100
99
  const columnType = (column as Knex.ColumnInfo & { columnType?: string }).columnType;
101
100
  if (columnType) result.columnType = columnType;
101
+ result.default = this._coerceColumnValue(result, column.defaultValue);
102
102
  return result;
103
103
  }
104
104
 
105
- protected _isMysqlBooleanColumn(column: ITableColumn) {
105
+ protected _isMysqlBooleanColumn(column: Pick<ITableColumn, 'columnType'>) {
106
106
  return column.columnType?.trim().toLowerCase() === 'tinyint(1)';
107
107
  }
108
108
 
@@ -191,34 +191,105 @@ export class ServiceDatabaseDialectBase extends BeanBase {
191
191
  return [items.map(item => item.id), builder];
192
192
  }
193
193
 
194
- protected _coerceColumnValue(type: string, value) {
194
+ protected _coerceColumnValue(column: Pick<ITableColumn, 'type' | 'columnType'>, value: unknown) {
195
195
  if (isNil(value)) return undefined;
196
- if (BOOLEAN_COLUMN_TYPES.includes(type)) return safeBoolean(value);
197
- if (NUMBER_COLUMN_TYPES.includes(type)) return this._safeNumber(value);
198
- if (
199
- this._columnTypePrefixes(type, TIMESTAMP_COLUMN_TYPE_PREFIXES) &&
200
- value === 'CURRENT_TIMESTAMP'
201
- ) {
202
- return undefined; // new Date();
196
+ if (typeof value !== 'string') {
197
+ return this._isBooleanColumn(column) ? safeBoolean(value as any) : value;
203
198
  }
204
- if (this._columnTypePrefixes(type, FLOAT_COLUMN_TYPE_PREFIXES)) {
205
- return this._safeNumber(value);
199
+
200
+ const literal = this._coerceColumnLiteral(value);
201
+ if (literal === undefined) return undefined;
202
+ if (this._isBooleanColumn(column)) return safeBoolean(literal.value);
203
+ if (literal.kind === 'boolean') return literal.value;
204
+
205
+ const type = this._columnTypeName(column.type);
206
+ if (literal.kind === 'string') return literal.value;
207
+ if (INTEGER_COLUMN_TYPES.includes(type)) return this._safeInteger(literal.value);
208
+ if (FLOAT_COLUMN_TYPES.includes(type)) return this._safeNumber(literal.value);
209
+ return literal.value;
210
+ }
211
+
212
+ protected _isBooleanColumn(column: Pick<ITableColumn, 'type' | 'columnType'>) {
213
+ return (
214
+ this._isMysqlBooleanColumn(column) ||
215
+ BOOLEAN_COLUMN_TYPES.includes(this._columnTypeName(column.type))
216
+ );
217
+ }
218
+
219
+ protected _columnTypeName(type: string) {
220
+ return type.trim().toLowerCase().match(/^\w+/)?.[0] ?? '';
221
+ }
222
+
223
+ protected _coerceColumnLiteral(value: string): ColumnDefaultLiteral | undefined {
224
+ const literal = this._unwrapColumnDefault(value);
225
+ if (literal === '') return undefined;
226
+ const literalLower = literal.toLowerCase();
227
+ if (literalLower === 'null' || GENERATED_DEFAULT_PATTERN.test(literal)) return undefined;
228
+ const quoted = literal.match(QUOTED_LITERAL_PATTERN);
229
+ if (quoted) return { kind: 'string', value: quoted[1].replaceAll("''", "'") };
230
+ if (literalLower === 'true' || literalLower === 't') return { kind: 'boolean', value: true };
231
+ if (literalLower === 'false' || literalLower === 'f') return { kind: 'boolean', value: false };
232
+ if (NUMBER_LITERAL_PATTERN.test(literal)) return { kind: 'number', value: literal };
233
+ if (this._isExpressionColumnDefault(literal)) return undefined;
234
+ return { kind: 'string', value: literal };
235
+ }
236
+
237
+ protected _unwrapColumnDefault(value: string) {
238
+ let literal = value.trim();
239
+ while (literal.startsWith('(') && literal.endsWith(')')) {
240
+ const inner = literal.slice(1, -1).trim();
241
+ if (!inner || !this._isColumnDefaultBalanced(inner)) break;
242
+ literal = inner;
206
243
  }
207
- if (this._columnTypePrefixes(type, INTEGER_COLUMN_TYPE_PREFIXES)) {
208
- return this._safeNumber(value);
244
+ const castIndex = literal.indexOf('::');
245
+ if (castIndex > 0 && this._isScalarColumnDefault(literal.slice(0, castIndex).trim())) {
246
+ literal = literal.slice(0, castIndex).trim();
209
247
  }
210
- // pg: NULL::character varying
211
- if (value.indexOf('NULL::') === 0) return undefined;
212
- return value;
248
+ return literal;
213
249
  }
214
250
 
215
- // pg: nextval
216
- protected _safeNumber(value) {
251
+ protected _isScalarColumnDefault(value: string) {
252
+ return (
253
+ value.toLowerCase() === 'null' ||
254
+ QUOTED_LITERAL_PATTERN.test(value) ||
255
+ value.toLowerCase() === 'true' ||
256
+ value.toLowerCase() === 'false' ||
257
+ NUMBER_LITERAL_PATTERN.test(value)
258
+ );
259
+ }
260
+
261
+ protected _isColumnDefaultBalanced(value: string) {
262
+ let depth = 0;
263
+ let quoted = false;
264
+ for (let index = 0; index < value.length; index++) {
265
+ const char = value[index];
266
+ if (char === "'") {
267
+ if (quoted && value[index + 1] === "'") {
268
+ index++;
269
+ } else {
270
+ quoted = !quoted;
271
+ }
272
+ } else if (!quoted && char === '(') {
273
+ depth++;
274
+ } else if (!quoted && char === ')') {
275
+ if (depth === 0) return false;
276
+ depth--;
277
+ }
278
+ }
279
+ return !quoted && depth === 0;
280
+ }
281
+
282
+ protected _isExpressionColumnDefault(value: string) {
283
+ return /[()*/+\-\s]/.test(value);
284
+ }
285
+
286
+ protected _safeInteger(value: string) {
217
287
  const num = Number(value);
218
- return Number.isNaN(num) ? undefined : num;
288
+ return Number.isSafeInteger(num) ? num : value;
219
289
  }
220
290
 
221
- protected _columnTypePrefixes(type: string, prefixes: string[]) {
222
- return prefixes.some(prefix => type.includes(prefix));
291
+ protected _safeNumber(value: string) {
292
+ const num = Number(value);
293
+ return Number.isFinite(num) ? num : undefined;
223
294
  }
224
295
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-permission",
3
- "version": "5.1.19",
3
+ "version": "5.1.20",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.83",
3
+ "version": "5.1.84",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli",
3
- "version": "1.2.104",
3
+ "version": "1.2.105",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli",
6
6
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  "@cabloy/process-helper": "^3.1.8",
45
45
  "fs-extra": "^11.3.5",
46
46
  "semver": "^7.6.2",
47
- "zova-cli-set-front": "^1.2.102"
47
+ "zova-cli-set-front": "^1.2.103"
48
48
  },
49
49
  "devDependencies": {
50
50
  "clean-package": "^2.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli-set-front",
3
- "version": "1.2.102",
3
+ "version": "1.2.103",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli-set-front",
6
6
  "keywords": [
@@ -70,7 +70,7 @@
70
70
  "vite": "^8.0.14",
71
71
  "yaml": "^2.8.3",
72
72
  "zova-openapi": "^1.1.17",
73
- "zova-vite": "^1.1.44"
73
+ "zova-vite": "^1.1.45"
74
74
  },
75
75
  "devDependencies": {
76
76
  "clean-package": "^2.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-vite",
3
- "version": "1.1.44",
3
+ "version": "1.1.45",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "zova vite",
6
6
  "keywords": [
@@ -40,7 +40,7 @@
40
40
  "@babel/plugin-proposal-decorators": "^7.29.0",
41
41
  "@babel/plugin-transform-class-properties": "^7.28.6",
42
42
  "@babel/plugin-transform-typescript": "^7.28.6",
43
- "@cabloy/dotenv": "^1.2.8",
43
+ "@cabloy/dotenv": "^1.2.9",
44
44
  "@cabloy/extend": "^3.2.8",
45
45
  "@cabloy/module-glob": "^5.3.18",
46
46
  "@cabloy/module-info": "^2.0.3",
@@ -1 +0,0 @@
1
- {"sessionId":"834e10b3-c7de-4e46-8255-347f58ac99c9","pid":21283,"procStart":"Tue Sep 1 21:20:05 2026","acquiredAt":1788301241462}
@@ -1,3 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2016-present CabloyJS