tina4-nodejs 3.13.131 → 3.13.133
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/CLAUDE.md +3 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +398 -302
- package/packages/core/dist/index.js +399 -302
- package/packages/core/public/js/tina4-dev-admin.min.js +148 -512
- package/packages/core/src/devAdmin.ts +0 -46
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/server.ts +129 -3
- package/packages/frond/dist/index.js +52 -104
- package/packages/frond/src/engine.ts +52 -100
- package/packages/orm/dist/index.js +399 -302
- package/packages/orm/src/baseModel.ts +64 -55
- package/packages/orm/src/index.ts +2 -0
- package/packages/orm/src/modelCollection.ts +149 -0
- package/packages/swagger/dist/index.js +150 -116
- package/packages/swagger/src/generator.ts +261 -154
- package/types/core/src/index.d.ts +1 -1
- package/types/core/src/server.d.ts +15 -0
- package/types/frond/src/engine.d.ts +11 -0
- package/types/orm/src/baseModel.d.ts +20 -5
- package/types/orm/src/index.d.ts +2 -0
- package/types/orm/src/modelCollection.d.ts +101 -0
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
adapterQuery, adapterFetch, adapterExecute, adapterFetchOne,
|
|
4
4
|
adapterStartTransaction, adapterCommit, adapterRollback,
|
|
5
5
|
adapterTableExists, adapterCreateTable, extractLastInsertId,
|
|
6
|
-
DEFAULT_ROW_CAP,
|
|
6
|
+
probeTotal, DEFAULT_ROW_CAP,
|
|
7
7
|
} from "./database.js";
|
|
8
|
+
import { ModelCollection } from "./modelCollection.js";
|
|
8
9
|
import { validate as validateFields } from "./validation.js";
|
|
9
10
|
import { QueryBuilder } from "./queryBuilder.js";
|
|
10
11
|
import { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
@@ -451,6 +452,43 @@ export class BaseModel {
|
|
|
451
452
|
return this.getDbColumn(this.getPkField());
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Shared read tail for the collection-returning finders (where / all / select
|
|
457
|
+
* / find filter-form / withTrashed). Runs the SAME two calls `db.fetch()`
|
|
458
|
+
* makes — the page fetch AND the COUNT probe over the SAME base SQL — hydrates
|
|
459
|
+
* the rows into model instances, and returns a ModelCollection carrying the
|
|
460
|
+
* total (ADR-0064).
|
|
461
|
+
*
|
|
462
|
+
* The total is FREE: `probeTotal` is the exact `COUNT(*)` probe `db.fetch()`
|
|
463
|
+
* already runs; the ORM used to discard it. ZERO extra queries beyond that one
|
|
464
|
+
* probe. `sql` MUST NOT carry its own LIMIT/OFFSET — `adapterFetch` applies
|
|
465
|
+
* limit/offset to the page, and the probe wraps the un-limited SQL so it counts
|
|
466
|
+
* the WHOLE filtered set, not the page.
|
|
467
|
+
*/
|
|
468
|
+
protected static async _collect<T extends BaseModel>(
|
|
469
|
+
this: typeof BaseModel & (new (data?: Record<string, unknown>) => T),
|
|
470
|
+
sql: string,
|
|
471
|
+
params: unknown[] | undefined,
|
|
472
|
+
limit: number,
|
|
473
|
+
offset: number,
|
|
474
|
+
include?: string[],
|
|
475
|
+
): Promise<ModelCollection<T>> {
|
|
476
|
+
const db = this.getDb();
|
|
477
|
+
const rows = await adapterFetch(db, sql, params, limit, offset);
|
|
478
|
+
const data: Record<string, unknown>[] = Array.isArray(rows)
|
|
479
|
+
? (rows as Record<string, unknown>[])
|
|
480
|
+
: ((rows as { data?: Record<string, unknown>[] })?.data ?? []);
|
|
481
|
+
// The total comes from the fetch COUNT probe — NOT data.length (that is only
|
|
482
|
+
// the page). probeTotal returns undefined only for an unbounded read
|
|
483
|
+
// (limit <= 0), where the page IS the whole set, so data.length is right.
|
|
484
|
+
const total = (await probeTotal(db, sql, params, limit)) ?? data.length;
|
|
485
|
+
const instances = data.map((row) => new this(row) as T);
|
|
486
|
+
if (include) {
|
|
487
|
+
await (this as typeof BaseModel)._eagerLoad(instances as BaseModel[], include);
|
|
488
|
+
}
|
|
489
|
+
return new ModelCollection<T>(instances, total, limit, offset);
|
|
490
|
+
}
|
|
491
|
+
|
|
454
492
|
/**
|
|
455
493
|
* Find a record by primary key.
|
|
456
494
|
* @param id Primary key value.
|
|
@@ -531,7 +569,7 @@ export class BaseModel {
|
|
|
531
569
|
offset?: number,
|
|
532
570
|
orderBy?: string,
|
|
533
571
|
include?: string[],
|
|
534
|
-
): Promise<T
|
|
572
|
+
): Promise<ModelCollection<T>>;
|
|
535
573
|
static async find<T extends BaseModel>(
|
|
536
574
|
this: new (data?: Record<string, unknown>) => T,
|
|
537
575
|
filter?: Record<string, unknown> | number | string,
|
|
@@ -539,7 +577,7 @@ export class BaseModel {
|
|
|
539
577
|
offset = 0,
|
|
540
578
|
orderBy?: string,
|
|
541
579
|
include?: string[],
|
|
542
|
-
): Promise<T
|
|
580
|
+
): Promise<ModelCollection<T> | T | null> {
|
|
543
581
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
544
582
|
|
|
545
583
|
// Scalar PK lookup routes to findById. A number or a string (but NOT a
|
|
@@ -555,7 +593,6 @@ export class BaseModel {
|
|
|
555
593
|
|
|
556
594
|
// Array form — coerce `limit` back to a number for the list path.
|
|
557
595
|
const lim = typeof limit === "number" ? limit : 100;
|
|
558
|
-
const db = ModelClass.getDb();
|
|
559
596
|
const conditions: string[] = [];
|
|
560
597
|
const params: unknown[] = [];
|
|
561
598
|
|
|
@@ -579,18 +616,7 @@ export class BaseModel {
|
|
|
579
616
|
sql += ` ORDER BY ${orderBy}`;
|
|
580
617
|
}
|
|
581
618
|
|
|
582
|
-
|
|
583
|
-
const data = (rows as any)?.data ?? rows;
|
|
584
|
-
const instances = (Array.isArray(data) ? data : []).map((row: Record<string, unknown>) => {
|
|
585
|
-
const inst = new this(row) as T;
|
|
586
|
-
return inst;
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
if (include) {
|
|
590
|
-
await ModelClass._eagerLoad(instances as BaseModel[], include);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
return instances;
|
|
619
|
+
return ModelClass._collect<T>(sql, params, lim, offset, include);
|
|
594
620
|
}
|
|
595
621
|
|
|
596
622
|
/**
|
|
@@ -671,9 +697,8 @@ export class BaseModel {
|
|
|
671
697
|
offset: number = 0,
|
|
672
698
|
include?: string[],
|
|
673
699
|
orderBy?: string,
|
|
674
|
-
): Promise<T
|
|
700
|
+
): Promise<ModelCollection<T>> {
|
|
675
701
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
676
|
-
const db = ModelClass.getDb();
|
|
677
702
|
|
|
678
703
|
const conditions: string[] = [];
|
|
679
704
|
if (ModelClass.softDelete) {
|
|
@@ -685,18 +710,14 @@ export class BaseModel {
|
|
|
685
710
|
|
|
686
711
|
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
687
712
|
const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
|
|
688
|
-
|
|
689
|
-
|
|
713
|
+
// No LIMIT/OFFSET embedded: _collect's adapterFetch applies them to the page
|
|
714
|
+
// and the COUNT probe wraps the un-limited SQL so the total is the whole set.
|
|
715
|
+
const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`;
|
|
690
716
|
|
|
691
717
|
// No bind parameters: the only conditions left are the framework's own
|
|
692
718
|
// softDelete / tableFilter literals. A caller-supplied filter belongs on
|
|
693
719
|
// where(), which binds its params properly.
|
|
694
|
-
|
|
695
|
-
const instances = rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
696
|
-
if (include) {
|
|
697
|
-
await ModelClass._eagerLoad(instances, include);
|
|
698
|
-
}
|
|
699
|
-
return instances;
|
|
720
|
+
return ModelClass._collect<T>(sql, [], limit, offset, include);
|
|
700
721
|
}
|
|
701
722
|
|
|
702
723
|
/**
|
|
@@ -718,9 +739,8 @@ export class BaseModel {
|
|
|
718
739
|
offset: number = 0,
|
|
719
740
|
include?: string[],
|
|
720
741
|
orderBy?: string,
|
|
721
|
-
): Promise<T
|
|
742
|
+
): Promise<ModelCollection<T>> {
|
|
722
743
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
723
|
-
const db = ModelClass.getDb();
|
|
724
744
|
|
|
725
745
|
const parts: string[] = [];
|
|
726
746
|
if (ModelClass.softDelete) {
|
|
@@ -732,14 +752,11 @@ export class BaseModel {
|
|
|
732
752
|
parts.push(`(${conditions})`);
|
|
733
753
|
|
|
734
754
|
const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
|
|
735
|
-
|
|
755
|
+
// No LIMIT/OFFSET embedded — _collect applies them to the page and probes
|
|
756
|
+
// the total (ADR-0064). orderBy affects the page only; COUNT is order-free.
|
|
757
|
+
const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause}`;
|
|
736
758
|
|
|
737
|
-
|
|
738
|
-
const instances = rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
739
|
-
if (include) {
|
|
740
|
-
await ModelClass._eagerLoad(instances, include);
|
|
741
|
-
}
|
|
742
|
-
return instances;
|
|
759
|
+
return ModelClass._collect<T>(sql, params, limit, offset, include);
|
|
743
760
|
}
|
|
744
761
|
|
|
745
762
|
/**
|
|
@@ -1418,17 +1435,14 @@ export class BaseModel {
|
|
|
1418
1435
|
params?: unknown[],
|
|
1419
1436
|
limit: number = DEFAULT_ROW_CAP,
|
|
1420
1437
|
offset: number = 0,
|
|
1421
|
-
): Promise<T
|
|
1438
|
+
): Promise<ModelCollection<T>> {
|
|
1422
1439
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
1423
|
-
|
|
1424
|
-
//
|
|
1425
|
-
// a second one is a syntax
|
|
1426
|
-
//
|
|
1427
|
-
//
|
|
1428
|
-
|
|
1429
|
-
const paged = SQLTranslator.appendLimit(sql, limit, offset);
|
|
1430
|
-
const rows = await adapterQuery(db, paged, params);
|
|
1431
|
-
return rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
1440
|
+
// _collect runs the page fetch — the adapter applies limit/offset via
|
|
1441
|
+
// SQLTranslator.appendLimit, which scrubs literals/comments and skips when
|
|
1442
|
+
// the caller's SQL already carries its own LIMIT (a second one is a syntax
|
|
1443
|
+
// error on every engine) — AND the COUNT probe over the raw SQL, so the
|
|
1444
|
+
// ModelCollection carries the total for the filter (ADR-0064).
|
|
1445
|
+
return ModelClass._collect<T>(sql, params, limit, offset);
|
|
1432
1446
|
}
|
|
1433
1447
|
|
|
1434
1448
|
static async selectOne<T extends BaseModel>(
|
|
@@ -1518,9 +1532,8 @@ export class BaseModel {
|
|
|
1518
1532
|
params?: unknown[],
|
|
1519
1533
|
limit: number = DEFAULT_ROW_CAP,
|
|
1520
1534
|
offset: number = 0,
|
|
1521
|
-
): Promise<T
|
|
1535
|
+
): Promise<ModelCollection<T>> {
|
|
1522
1536
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
1523
|
-
const db = ModelClass.getDb();
|
|
1524
1537
|
|
|
1525
1538
|
const parts: string[] = [];
|
|
1526
1539
|
if (ModelClass.tableFilter) {
|
|
@@ -1534,15 +1547,11 @@ export class BaseModel {
|
|
|
1534
1547
|
if (parts.length > 0) {
|
|
1535
1548
|
sql += ` WHERE ${parts.join(" AND ")}`;
|
|
1536
1549
|
}
|
|
1537
|
-
if (limit !== undefined) {
|
|
1538
|
-
sql += ` LIMIT ${limit}`;
|
|
1539
|
-
}
|
|
1540
|
-
if (offset !== undefined) {
|
|
1541
|
-
sql += ` OFFSET ${offset}`;
|
|
1542
|
-
}
|
|
1543
1550
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1551
|
+
// No LIMIT/OFFSET embedded — _collect applies them to the page and probes
|
|
1552
|
+
// the total. No is_deleted filter here, so soft-deleted rows are INCLUDED in
|
|
1553
|
+
// both the page and the total (ADR-0064).
|
|
1554
|
+
return ModelClass._collect<T>(sql, params, limit, offset);
|
|
1546
1555
|
}
|
|
1547
1556
|
|
|
1548
1557
|
/**
|
|
@@ -13,6 +13,8 @@ export { REQUIRED_ADAPTER_CAPABILITIES, NOT_REQUIRED_ON_ADAPTER } from "./types.
|
|
|
13
13
|
|
|
14
14
|
export { DatabaseResult } from "./databaseResult.js";
|
|
15
15
|
export type { ColumnInfoResult } from "./databaseResult.js";
|
|
16
|
+
export { ModelCollection } from "./modelCollection.js";
|
|
17
|
+
export type { PaginateEnvelope } from "./modelCollection.js";
|
|
16
18
|
export { Database, initDatabase, getAdapter, setAdapter, bindDatabase, createAdapterFromUrl, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons, wrapWithCache, resetRequestCaches } from "./database.js";
|
|
17
19
|
export {
|
|
18
20
|
adapterFetch, adapterQuery, adapterFetchOne, adapterExecute, adapterInsert,
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelCollection — a list of ORM models that also carries the query total.
|
|
3
|
+
*
|
|
4
|
+
* What the ORM read queries (`where` / `select` / `find` filter-form / `all` /
|
|
5
|
+
* `withTrashed`) return. It IS an Array — iterate it, index it, slice it, `.map`
|
|
6
|
+
* it, `.filter` it, read `.length`, `JSON.stringify` it — so every existing
|
|
7
|
+
* caller keeps working unchanged (`Array.isArray(coll) === true`). It adds one
|
|
8
|
+
* thing: the TOTAL number of rows matching the query's filter, independent of
|
|
9
|
+
* `limit` / `offset`.
|
|
10
|
+
*
|
|
11
|
+
* The total is free. Every one of those methods already runs the fetch COUNT
|
|
12
|
+
* probe (`db.fetch` / `probeTotal`) that computes `SELECT COUNT(*)` for the same
|
|
13
|
+
* filter; the ORM used to hydrate the page of models and throw that count away.
|
|
14
|
+
* This class carries it instead, so a caller with 20 models on the page can
|
|
15
|
+
* still learn there are 250 rows in the set. ZERO extra queries.
|
|
16
|
+
*
|
|
17
|
+
* Uniform across all four Tina4 frameworks (ADR-0064). Same concept, language-
|
|
18
|
+
* idiomatic accessor name:
|
|
19
|
+
*
|
|
20
|
+
* Python / Ruby : get_total_records() to_paginate()
|
|
21
|
+
* PHP / Node : getTotalRecords() toPaginate()
|
|
22
|
+
*
|
|
23
|
+
* The accessor is a METHOD, not a `.total` property, on purpose: `Array#count`
|
|
24
|
+
* exists in Ruby and `list.count()` in Python, so a `.count` would shadow a
|
|
25
|
+
* built-in. `DatabaseResult` keeps its `.count` property (it is not a list); both
|
|
26
|
+
* expose the identical seven-key `toPaginate()` envelope.
|
|
27
|
+
*
|
|
28
|
+
* ### Why a real Array subclass with `[Symbol.species] === Array`
|
|
29
|
+
*
|
|
30
|
+
* `class X extends Array` is the parity-faithful backing (the ADR-0064 table
|
|
31
|
+
* says Node is a "subclass of Array"), and it keeps `Array.isArray` true and
|
|
32
|
+
* `instanceof ModelCollection` true (mirroring Python's `isinstance`). BUT a bare
|
|
33
|
+
* Array subclass is a footgun: `map`/`filter`/`slice` build the result via
|
|
34
|
+
* `this.constructor[Symbol.species]`, calling the constructor with a single
|
|
35
|
+
* NUMBER (the length), and `new X(oneNumber)` means "length N", not "one
|
|
36
|
+
* element". Overriding `[Symbol.species]` to return `Array` makes every derived
|
|
37
|
+
* operation build a plain `Array` — so `.map`/`.filter`/`.slice`/spread never
|
|
38
|
+
* touch this constructor and can never explode. The constructor below is also
|
|
39
|
+
* defended against the numeric-length call directly, so even a path that ignores
|
|
40
|
+
* species is safe.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** The canonical pagination envelope — seven snake_case keys (ADR-0043/0064). */
|
|
44
|
+
export interface PaginateEnvelope {
|
|
45
|
+
records: unknown[];
|
|
46
|
+
total: number;
|
|
47
|
+
page: number;
|
|
48
|
+
per_page: number;
|
|
49
|
+
total_pages: number;
|
|
50
|
+
limit: number;
|
|
51
|
+
offset: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ModelCollection<T = unknown> extends Array<T> {
|
|
55
|
+
/** Total rows matching the query's filter (ignores limit/offset). */
|
|
56
|
+
private _total = 0;
|
|
57
|
+
/** The SQL limit that produced this page. */
|
|
58
|
+
private _limit = 0;
|
|
59
|
+
/** The SQL offset that produced this page. */
|
|
60
|
+
private _offset = 0;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Derived array operations (`map`, `filter`, `slice`, spread, …) build a plain
|
|
64
|
+
* `Array`, never another `ModelCollection`. This is what defuses the Array-
|
|
65
|
+
* subclass constructor/species trap: the engine never calls this constructor
|
|
66
|
+
* with a length during those operations.
|
|
67
|
+
*/
|
|
68
|
+
static get [Symbol.species](): ArrayConstructor {
|
|
69
|
+
return Array;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param items the page of hydrated model instances (or the length, when the
|
|
74
|
+
* engine constructs a derived array — defended against here).
|
|
75
|
+
* @param total total rows matching the query's filter (ignores limit/offset).
|
|
76
|
+
* @param limit the SQL limit that produced this page.
|
|
77
|
+
* @param offset the SQL offset that produced this page.
|
|
78
|
+
*/
|
|
79
|
+
constructor(items?: readonly T[] | number, total = 0, limit = 0, offset = 0) {
|
|
80
|
+
super();
|
|
81
|
+
// Defensive: a raw `new ModelCollection(5)` (or any engine path that ignores
|
|
82
|
+
// [Symbol.species]) must behave like `new Array(5)` — a length-5 array — not
|
|
83
|
+
// wedge a number into element 0. `[Symbol.species] = Array` means the built-
|
|
84
|
+
// in derived ops never reach here, but this keeps the constructor honest.
|
|
85
|
+
if (typeof items === "number") {
|
|
86
|
+
this.length = items;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (items) {
|
|
90
|
+
for (let i = 0; i < items.length; i++) {
|
|
91
|
+
this[i] = items[i];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this._total = Math.trunc(total) || 0;
|
|
95
|
+
this._limit = Math.trunc(limit) || 0;
|
|
96
|
+
this._offset = Math.trunc(offset) || 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Total rows matching the query's filter, ignoring limit/offset.
|
|
101
|
+
*
|
|
102
|
+
* This is the whole point of the collection: the page slice you are iterating
|
|
103
|
+
* is capped by `limit`, but this number is the full count of matching rows —
|
|
104
|
+
* what a pager needs to render "page 3 of 13".
|
|
105
|
+
*/
|
|
106
|
+
getTotalRecords(): number {
|
|
107
|
+
return this._total;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The canonical pagination envelope — seven snake_case keys, identical to
|
|
112
|
+
* `DatabaseResult.toPaginate()` (ADR-0043) and to the other three frameworks'
|
|
113
|
+
* `toPaginate()` / `to_paginate()`.
|
|
114
|
+
*
|
|
115
|
+
* records the page's rows as plain objects (never re-sliced)
|
|
116
|
+
* total getTotalRecords() — the true total for the filter
|
|
117
|
+
* page floor(offset / per_page) + 1
|
|
118
|
+
* per_page the query's limit
|
|
119
|
+
* total_pages ceil(total / per_page)
|
|
120
|
+
* limit the SQL limit actually applied
|
|
121
|
+
* offset the SQL offset actually applied
|
|
122
|
+
*
|
|
123
|
+
* `records` are model dicts (via `toDict()`, the same serialisation the
|
|
124
|
+
* framework applies to a model in a JSON response), so the JSON a client sees
|
|
125
|
+
* matches `DatabaseResult` exactly — the result is uniform whether the route
|
|
126
|
+
* returned a raw `db.fetch()` or an ORM query.
|
|
127
|
+
*/
|
|
128
|
+
toPaginate(): PaginateEnvelope {
|
|
129
|
+
const perPage = this._limit > 0 ? this._limit : this.length;
|
|
130
|
+
const page = perPage > 0 ? Math.floor(this._offset / perPage) + 1 : 1;
|
|
131
|
+
const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this._total / perPage)) : 1;
|
|
132
|
+
// `.map` returns a plain Array here (see [Symbol.species]). Each model is
|
|
133
|
+
// serialised via toDict() — the same shape the framework emits for a model
|
|
134
|
+
// in a JSON response — so the envelope matches DatabaseResult exactly.
|
|
135
|
+
const records = this.map((model) => {
|
|
136
|
+
const m = model as unknown as { toDict?: () => unknown };
|
|
137
|
+
return m && typeof m.toDict === "function" ? m.toDict() : model;
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
records,
|
|
141
|
+
total: this._total,
|
|
142
|
+
page,
|
|
143
|
+
per_page: perPage,
|
|
144
|
+
total_pages: totalPages,
|
|
145
|
+
limit: perPage,
|
|
146
|
+
offset: this._offset,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|