@navette-js/database 1.0.0-beta.1 → 1.0.0-beta.2
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 +1 -1
- package/lib/index.d.ts +25 -24
- package/lib/index.js +19 -18
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Data adapters (Objection/Knex) for Navette panels.
|
|
4
4
|
|
|
5
|
-
Part of [Navette](https://github.com/NavetteJS/navette),
|
|
5
|
+
Part of [Navette](https://github.com/NavetteJS/navette), admin panels for Node.js. Start a project with `npm create navette@latest`, or install the framework with `npm install @navette-js/panels`.
|
|
6
6
|
|
|
7
7
|
**Beta:** the API may still change between beta releases.
|
|
8
8
|
|
package/lib/index.d.ts
CHANGED
|
@@ -4,23 +4,23 @@ export { Model };
|
|
|
4
4
|
export type { Knex };
|
|
5
5
|
export type SortDirection = 'asc' | 'desc';
|
|
6
6
|
export type ListQuery = {
|
|
7
|
-
/**
|
|
7
|
+
/** Global search: every word must appear in at least one of the searchable columns. */
|
|
8
8
|
search?: string | null;
|
|
9
9
|
searchColumns?: string[];
|
|
10
|
-
/**
|
|
10
|
+
/** Per-column search: all conditions must match. */
|
|
11
11
|
columnSearches?: Record<string, string>;
|
|
12
|
-
/**
|
|
12
|
+
/** Soft-deleted records: excluded by default, included, or only those. */
|
|
13
13
|
trashed?: 'without' | 'with' | 'only';
|
|
14
14
|
sort?: {
|
|
15
15
|
column: string;
|
|
16
16
|
direction: SortDirection;
|
|
17
17
|
} | null;
|
|
18
|
-
/**
|
|
18
|
+
/** Table grouping: this sort is applied before `sort`. */
|
|
19
19
|
groupSort?: {
|
|
20
20
|
column: string;
|
|
21
21
|
direction: SortDirection;
|
|
22
22
|
} | null;
|
|
23
|
-
/**
|
|
23
|
+
/** Extra server-side conditions (list tab, custom filter); never built from unvalidated browser input. */
|
|
24
24
|
modifiers?: ((query: any) => void)[];
|
|
25
25
|
page: number;
|
|
26
26
|
perPage: number;
|
|
@@ -34,13 +34,13 @@ export type Page<T> = {
|
|
|
34
34
|
from: number | null;
|
|
35
35
|
to: number | null;
|
|
36
36
|
};
|
|
37
|
-
/**
|
|
37
|
+
/** What the framework needs to list, read and write records, independently of the ORM. */
|
|
38
38
|
export interface DataAdapter<T = any> {
|
|
39
39
|
getKey(record: T): string;
|
|
40
|
-
/**
|
|
40
|
+
/** Filters keys received from the browser: only those within the adapter's scope are returned. */
|
|
41
41
|
keysInScope(keys: (string | number)[]): Promise<string[]>;
|
|
42
42
|
sum(query: Omit<ListQuery, 'page' | 'perPage'>, columns: string[]): Promise<Record<string, number>>;
|
|
43
|
-
/**
|
|
43
|
+
/** Aggregates over the records of a list query: count, and column sums, averages, minimums and maximums. */
|
|
44
44
|
aggregate(query: Omit<ListQuery, 'page' | 'perPage'>, columns?: {
|
|
45
45
|
sum?: string[];
|
|
46
46
|
avg?: string[];
|
|
@@ -60,27 +60,29 @@ export interface DataAdapter<T = any> {
|
|
|
60
60
|
create(data: Record<string, unknown>): Promise<T>;
|
|
61
61
|
update(key: string | number, data: Record<string, unknown>): Promise<T | null>;
|
|
62
62
|
delete(key: string | number): Promise<boolean>;
|
|
63
|
-
/**
|
|
63
|
+
/** Soft deletes: support, record state, restore, permanent delete. */
|
|
64
64
|
supportsSoftDeletes(): boolean;
|
|
65
65
|
isTrashed(record: T): boolean;
|
|
66
66
|
restore(key: string | number): Promise<boolean>;
|
|
67
67
|
forceDelete(key: string | number): Promise<boolean>;
|
|
68
|
-
/**
|
|
68
|
+
/** Keys of every record matching the query (unpaginated), for "select all". */
|
|
69
69
|
listKeys(query: Omit<ListQuery, 'page' | 'perPage'>): Promise<string[]>;
|
|
70
70
|
deleteMany(keys: (string | number)[]): Promise<number>;
|
|
71
|
-
/**
|
|
71
|
+
/** True if another record already has this value (`unique` rule). */
|
|
72
72
|
exists(column: string, value: unknown, exceptKey?: string | number | null): Promise<boolean>;
|
|
73
73
|
}
|
|
74
74
|
export declare function createConnection(config: Knex.Config): Knex;
|
|
75
|
-
/**
|
|
75
|
+
/** SQLite connection (better-sqlite3 driver), for development and demos. */
|
|
76
76
|
export declare function sqlite(filename: string): Knex;
|
|
77
77
|
export declare class ObjectionAdapter<M extends Model> implements DataAdapter<M> {
|
|
78
78
|
private model;
|
|
79
79
|
private modify;
|
|
80
80
|
private softDeletes;
|
|
81
81
|
private findsTrashed;
|
|
82
|
-
/**
|
|
83
|
-
*
|
|
82
|
+
/**
|
|
83
|
+
* `modify`: base query constraints applied to every read. `softDeletes`: deletion timestamp column.
|
|
84
|
+
* `findsTrashed`: `find()` also returns soft-deleted records.
|
|
85
|
+
*/
|
|
84
86
|
constructor(model: ModelClass<M>, options?: {
|
|
85
87
|
modify?: (query: QueryBuilder<M, M[]>) => void;
|
|
86
88
|
softDeletes?: string;
|
|
@@ -88,8 +90,8 @@ export declare class ObjectionAdapter<M extends Model> implements DataAdapter<M>
|
|
|
88
90
|
});
|
|
89
91
|
private tenantScope;
|
|
90
92
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
+
* Scopes the adapter to a tenant: every read filters on `column` and every create fills it, with the key returned by `key()`
|
|
94
|
+
* (the request's tenant; `null` means no filter). Set by the panel (`panel.tenant()`).
|
|
93
95
|
*/
|
|
94
96
|
scopeToTenant(column: string, key: () => string | number | null): this;
|
|
95
97
|
private base;
|
|
@@ -100,11 +102,11 @@ export declare class ObjectionAdapter<M extends Model> implements DataAdapter<M>
|
|
|
100
102
|
count(): Promise<number>;
|
|
101
103
|
listKeys(query: Omit<ListQuery, 'page' | 'perPage'>): Promise<string[]>;
|
|
102
104
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
+
* Keeps the browser-supplied keys that fall within the adapter's scope (`modify` and tenant; trashed records included),
|
|
106
|
+
* so a forged selection cannot target a record the table does not show.
|
|
105
107
|
*/
|
|
106
108
|
keysInScope(keys: (string | number)[]): Promise<string[]>;
|
|
107
|
-
/**
|
|
109
|
+
/** Count and column aggregates over every record of the query (for summary rows). */
|
|
108
110
|
aggregate(query: Omit<ListQuery, 'page' | 'perPage'>, columns?: {
|
|
109
111
|
sum?: string[];
|
|
110
112
|
avg?: string[];
|
|
@@ -118,9 +120,8 @@ export declare class ObjectionAdapter<M extends Model> implements DataAdapter<M>
|
|
|
118
120
|
max: Record<string, unknown>;
|
|
119
121
|
}>;
|
|
120
122
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* SQL de date écrit pour SQLite ; dates en UTC, au format `Y-m-d H:i:s`.
|
|
123
|
+
* Monthly trend over the filtered query (count, or average of `average`): one value per month from `start` to `end`,
|
|
124
|
+
* 0 for months without records. The date SQL targets SQLite; dates are UTC, formatted `Y-m-d H:i:s`.
|
|
124
125
|
*/
|
|
125
126
|
trendPerMonth(query: Omit<ListQuery, 'page' | 'perPage'>, options: {
|
|
126
127
|
start: Date;
|
|
@@ -132,13 +133,13 @@ export declare class ObjectionAdapter<M extends Model> implements DataAdapter<M>
|
|
|
132
133
|
sum(query: Omit<ListQuery, 'page' | 'perPage'>, columns: string[]): Promise<Record<string, number>>;
|
|
133
134
|
deleteMany(keys: (string | number)[]): Promise<number>;
|
|
134
135
|
paginate(query: ListQuery): Promise<Page<M>>;
|
|
135
|
-
/**
|
|
136
|
+
/** Base query with modifiers, global search and per-column searches applied. */
|
|
136
137
|
private filtered;
|
|
137
138
|
private finishPagination;
|
|
138
139
|
find(key: string | number): Promise<M | null>;
|
|
139
140
|
create(data: Record<string, unknown>): Promise<M>;
|
|
140
141
|
update(key: string | number, data: Record<string, unknown>): Promise<M | null>;
|
|
141
|
-
/**
|
|
142
|
+
/** With soft deletes, the record is timestamped instead of removed. */
|
|
142
143
|
delete(key: string | number): Promise<boolean>;
|
|
143
144
|
supportsSoftDeletes(): boolean;
|
|
144
145
|
isTrashed(record: M): boolean;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @navette-js/database —
|
|
1
|
+
// @navette-js/database — data access contract and its Knex + Objection adapter.
|
|
2
2
|
import knexFactory from 'knex';
|
|
3
3
|
import { Model } from 'objection';
|
|
4
4
|
export { Model };
|
|
@@ -7,7 +7,7 @@ export function createConnection(config) {
|
|
|
7
7
|
Model.knex(knex);
|
|
8
8
|
return knex;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/** SQLite connection (better-sqlite3 driver), for development and demos. */
|
|
11
11
|
export function sqlite(filename) {
|
|
12
12
|
return createConnection({ client: 'better-sqlite3', connection: { filename }, useNullAsDefault: true });
|
|
13
13
|
}
|
|
@@ -17,8 +17,10 @@ export class ObjectionAdapter {
|
|
|
17
17
|
modify;
|
|
18
18
|
softDeletes;
|
|
19
19
|
findsTrashed;
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
20
|
+
/**
|
|
21
|
+
* `modify`: base query constraints applied to every read. `softDeletes`: deletion timestamp column.
|
|
22
|
+
* `findsTrashed`: `find()` also returns soft-deleted records.
|
|
23
|
+
*/
|
|
22
24
|
constructor(model, options = {}) {
|
|
23
25
|
this.model = model;
|
|
24
26
|
this.modify = options.modify ?? null;
|
|
@@ -27,8 +29,8 @@ export class ObjectionAdapter {
|
|
|
27
29
|
}
|
|
28
30
|
tenantScope = null;
|
|
29
31
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* Scopes the adapter to a tenant: every read filters on `column` and every create fills it, with the key returned by `key()`
|
|
33
|
+
* (the request's tenant; `null` means no filter). Set by the panel (`panel.tenant()`).
|
|
32
34
|
*/
|
|
33
35
|
scopeToTenant(column, key) { this.tenantScope = { column, key }; return this; }
|
|
34
36
|
base(trashed = 'without') {
|
|
@@ -56,8 +58,8 @@ export class ObjectionAdapter {
|
|
|
56
58
|
return rows.map((r) => this.getKey(r));
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
+
* Keeps the browser-supplied keys that fall within the adapter's scope (`modify` and tenant; trashed records included),
|
|
62
|
+
* so a forged selection cannot target a record the table does not show.
|
|
61
63
|
*/
|
|
62
64
|
async keysInScope(keys) {
|
|
63
65
|
if (!keys.length)
|
|
@@ -65,7 +67,7 @@ export class ObjectionAdapter {
|
|
|
65
67
|
const rows = await this.base('with').whereIn(`${this.model.tableName}.${this.keyColumn}`, keys.map(String).slice(0, 10000));
|
|
66
68
|
return rows.map((r) => this.getKey(r));
|
|
67
69
|
}
|
|
68
|
-
/**
|
|
70
|
+
/** Count and column aggregates over every record of the query (for summary rows). */
|
|
69
71
|
async aggregate(query, columns = {}) {
|
|
70
72
|
const q = this.model.knex().from(this.filtered(query).toKnexQuery().as('t')).count({ __count: '*' });
|
|
71
73
|
for (const column of columns.sum ?? [])
|
|
@@ -86,9 +88,8 @@ export class ObjectionAdapter {
|
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
90
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* SQL de date écrit pour SQLite ; dates en UTC, au format `Y-m-d H:i:s`.
|
|
91
|
+
* Monthly trend over the filtered query (count, or average of `average`): one value per month from `start` to `end`,
|
|
92
|
+
* 0 for months without records. The date SQL targets SQLite; dates are UTC, formatted `Y-m-d H:i:s`.
|
|
92
93
|
*/
|
|
93
94
|
async trendPerMonth(query, options) {
|
|
94
95
|
const pad = (n) => String(n).padStart(2, '0');
|
|
@@ -102,7 +103,7 @@ export class ObjectionAdapter {
|
|
|
102
103
|
options.modify?.(q);
|
|
103
104
|
const rows = new Map((await q).map((row) => [row.month, Number(row.aggregate ?? 0)]));
|
|
104
105
|
const values = [];
|
|
105
|
-
//
|
|
106
|
+
// One step per calendar month from `start`, up to and including `end`.
|
|
106
107
|
for (let i = 0;; i++) {
|
|
107
108
|
const month = new Date(Date.UTC(options.start.getUTCFullYear(), options.start.getUTCMonth() + i, 1));
|
|
108
109
|
if (month.getTime() > options.end.getTime())
|
|
@@ -114,7 +115,7 @@ export class ObjectionAdapter {
|
|
|
114
115
|
async sum(query, columns) {
|
|
115
116
|
if (!columns.length)
|
|
116
117
|
return {};
|
|
117
|
-
//
|
|
118
|
+
// The filtered query is used as a subquery so its computed (aliased) columns remain usable by the search.
|
|
118
119
|
const q = this.model.knex().from(this.filtered(query).toKnexQuery().as('t'));
|
|
119
120
|
for (const column of columns)
|
|
120
121
|
q.sum({ [column]: `t.${column}` });
|
|
@@ -132,7 +133,7 @@ export class ObjectionAdapter {
|
|
|
132
133
|
async paginate(query) {
|
|
133
134
|
return this.finishPagination(this.filtered(query), query);
|
|
134
135
|
}
|
|
135
|
-
/**
|
|
136
|
+
/** Base query with modifiers, global search and per-column searches applied. */
|
|
136
137
|
filtered(query) {
|
|
137
138
|
const q = this.base(query.trashed ?? 'without');
|
|
138
139
|
for (const modifier of query.modifiers ?? [])
|
|
@@ -140,7 +141,7 @@ export class ObjectionAdapter {
|
|
|
140
141
|
const words = (query.search ?? '').trim().split(/\s+/).filter(Boolean);
|
|
141
142
|
const columns = query.searchColumns ?? [];
|
|
142
143
|
if (words.length && columns.length) {
|
|
143
|
-
//
|
|
144
|
+
// Each word must match at least one column, case-insensitively; LIKE wildcards in the input are escaped.
|
|
144
145
|
for (const word of words) {
|
|
145
146
|
q.where((sub) => {
|
|
146
147
|
for (const column of columns)
|
|
@@ -173,7 +174,7 @@ export class ObjectionAdapter {
|
|
|
173
174
|
return (await this.model.query().insertAndFetch((tenantKey !== null ? { ...data, [this.tenantScope.column]: tenantKey } : data)));
|
|
174
175
|
}
|
|
175
176
|
async update(key, data) {
|
|
176
|
-
//
|
|
177
|
+
// Records outside the current tenant cannot be updated, and the tenant column never changes.
|
|
177
178
|
if (this.tenantScope && this.tenantScope.key() !== null) {
|
|
178
179
|
if (!(await this.base('with').findById(key)))
|
|
179
180
|
return null;
|
|
@@ -181,7 +182,7 @@ export class ObjectionAdapter {
|
|
|
181
182
|
}
|
|
182
183
|
return (await this.model.query().patchAndFetchById(key, data)) ?? null;
|
|
183
184
|
}
|
|
184
|
-
/**
|
|
185
|
+
/** With soft deletes, the record is timestamped instead of removed. */
|
|
185
186
|
async delete(key) {
|
|
186
187
|
if (this.softDeletes)
|
|
187
188
|
return (await this.model.query().findById(key).whereNull(this.softDeletes).patch({ [this.softDeletes]: this.now() })) > 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/database",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"description": "Data adapters (Objection/Knex) for Navette panels.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"navette",
|
|
33
|
-
"filament",
|
|
34
33
|
"admin",
|
|
35
34
|
"panel",
|
|
36
35
|
"livewire",
|