@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 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), a Node.js port of [Filament](https://filamentphp.com). Install the whole framework with `npm install @navette-js/panels`.
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
- /** Recherche globale : chaque mot doit apparaître dans au moins une des colonnes cherchables. */
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
- /** Recherche par colonne : toutes les conditions doivent être vraies. */
10
+ /** Per-column search: all conditions must match. */
11
11
  columnSearches?: Record<string, string>;
12
- /** Enregistrements supprimés (suppression douce) : exclus par défaut, inclus, ou seuls. */
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
- /** Regroupement de table : ce tri passe avant `sort`. */
18
+ /** Table grouping: this sort is applied before `sort`. */
19
19
  groupSort?: {
20
20
  column: string;
21
21
  direction: SortDirection;
22
22
  } | null;
23
- /** Conditions supplémentaires posées par le serveur (onglet de liste, filtre sur mesure) : jamais construites à partir de données du navigateur non validées. */
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
- /** Ce dont le framework a besoin pour lister, lire et écrire des enregistrements, sans connaître l'ORM. */
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
- /** Filtre des clés reçues du navigateur : seules celles de la portée de l'adaptateur sont rendues. */
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
- /** Agrégats sur les enregistrements d'une requête de liste : nombre, sommes et moyennes de colonnes. */
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
- /** Suppression douce : prise en charge, état d'un enregistrement, restauration, suppression définitive. */
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
- /** Clés de tous les enregistrements correspondant à la requête (sans pagination), pour « tout sélectionner ». */
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
- /** Vrai si un autre enregistrement a déjà cette valeur (règle `unique`). */
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
- /** Connexion SQLite (pilote better-sqlite3), pour le développement et la démo. */
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
- /** `softDeletes` : colonne d'horodatage de suppression (équivalent du trait SoftDeletes). `findsTrashed` : `find()` voit aussi les supprimés
83
- * (équivalent de `getRecordRouteBindingEloquentQuery()->withoutGlobalScopes([SoftDeletingScope::class])`). */
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
- * Cloisonne l'adaptateur par locataire : toute lecture filtre sur `column`, toute création la renseigne,
92
- * avec la clé rendue par `key()` (celle du locataire de la requête ; `null` : pas de filtre). Posé par le panel (`panel.tenant()`).
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
- * Parmi des clés reçues du navigateur, celles qui appartiennent à la portée de l'adaptateur (`modify`, suppression douce comprise).
104
- * Une sélection forgée ne peut ainsi pas viser un enregistrement que la table n'affiche pas.
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
- /** Somme de colonnes sur tous les enregistrements de la requête (pour les lignes de totaux). */
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
- * Tendance mensuelle sur la requête filtrée (équivalent de `Trend::query(...)->between()->perMonth()->count()` ou `->average()`
122
- * de flowframe/laravel-trend) : une valeur par mois de `start` à `end`, 0 pour un mois sans enregistrement.
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
- /** Requête de base avec recherche globale et recherches par colonne appliquées. */
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
- /** Avec suppression douce, l'enregistrement est horodaté, pas retiré. */
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 — contrat d'accès aux données (ADR 0005) et premier adaptateur : Knex + Objection.
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
- /** Connexion SQLite (pilote better-sqlite3), pour le développement et la démo. */
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
- /** `softDeletes` : colonne d'horodatage de suppression (équivalent du trait SoftDeletes). `findsTrashed` : `find()` voit aussi les supprimés
21
- * (équivalent de `getRecordRouteBindingEloquentQuery()->withoutGlobalScopes([SoftDeletingScope::class])`). */
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
- * Cloisonne l'adaptateur par locataire : toute lecture filtre sur `column`, toute création la renseigne,
31
- * avec la clé rendue par `key()` (celle du locataire de la requête ; `null` : pas de filtre). Posé par le panel (`panel.tenant()`).
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
- * Parmi des clés reçues du navigateur, celles qui appartiennent à la portée de l'adaptateur (`modify`, suppression douce comprise).
60
- * Une sélection forgée ne peut ainsi pas viser un enregistrement que la table n'affiche pas.
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
- /** Somme de colonnes sur tous les enregistrements de la requête (pour les lignes de totaux). */
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
- * Tendance mensuelle sur la requête filtrée (équivalent de `Trend::query(...)->between()->perMonth()->count()` ou `->average()`
90
- * de flowframe/laravel-trend) : une valeur par mois de `start` à `end`, 0 pour un mois sans enregistrement.
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
- // Comme CarbonPeriod par pas d'un mois : même jour du mois que `start`, jusqu'à `end` compris.
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
- // La requête filtrée sert de sous-requête : ses colonnes calculées (alias) restent utilisables par la recherche.
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
- /** Requête de base avec recherche globale et recherches par colonne appliquées. */
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
- // Comme Filament : chaque mot doit correspondre à au moins une colonne, sans tenir compte de la casse.
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
- // Un enregistrement hors du locataire courant n'est pas modifiable, et la colonne du locataire ne change pas.
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
- /** Avec suppression douce, l'enregistrement est horodaté, pas retiré. */
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.1",
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",