@drax/crud-back 3.1.0 → 3.4.1

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.
@@ -6,6 +6,7 @@ class AbstractSqliteRepository {
6
6
  this.tableName = '';
7
7
  this.searchFields = [];
8
8
  this.booleanFields = [];
9
+ this.jsonFields = [];
9
10
  this.identifier = '_id';
10
11
  if (!dataBaseFile) {
11
12
  throw new Error("dataBaseFile is required");
@@ -30,6 +31,67 @@ class AbstractSqliteRepository {
30
31
  async prepareItem(item) {
31
32
  return item;
32
33
  }
34
+ parseJsonValue(value) {
35
+ if (typeof value !== 'string') {
36
+ return value;
37
+ }
38
+ try {
39
+ return JSON.parse(value);
40
+ }
41
+ catch {
42
+ return value;
43
+ }
44
+ }
45
+ normalizeSqliteValue(value) {
46
+ if (value === undefined) {
47
+ return null;
48
+ }
49
+ if (value === null) {
50
+ return null;
51
+ }
52
+ if (typeof value === 'boolean') {
53
+ return value ? 1 : 0;
54
+ }
55
+ if (typeof value === 'number' || typeof value === 'string' || typeof value === 'bigint') {
56
+ return value;
57
+ }
58
+ if (Buffer.isBuffer(value)) {
59
+ return value;
60
+ }
61
+ if (value instanceof Date) {
62
+ return value.toISOString();
63
+ }
64
+ if (value instanceof ArrayBuffer) {
65
+ return Buffer.from(value);
66
+ }
67
+ if (ArrayBuffer.isView(value)) {
68
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
69
+ }
70
+ if (typeof value === 'object') {
71
+ return JSON.stringify(value);
72
+ }
73
+ return String(value);
74
+ }
75
+ normalizeSqliteData(data) {
76
+ if (!data || typeof data !== 'object') {
77
+ return data;
78
+ }
79
+ for (const key of Object.keys(data)) {
80
+ data[key] = this.normalizeSqliteValue(data[key]);
81
+ }
82
+ return data;
83
+ }
84
+ deserializeSqliteData(item) {
85
+ if (!item || typeof item !== 'object') {
86
+ return item;
87
+ }
88
+ for (const field of this.jsonFields) {
89
+ if (Object.prototype.hasOwnProperty.call(item, field)) {
90
+ item[field] = this.parseJsonValue(item[field]);
91
+ }
92
+ }
93
+ return item;
94
+ }
33
95
  async execPopulate(item) {
34
96
  for (const field of this.populateFields) {
35
97
  if (item[field.field]) {
@@ -49,6 +111,7 @@ class AbstractSqliteRepository {
49
111
  }
50
112
  async decorate(item) {
51
113
  await this.execPopulate(item);
114
+ this.deserializeSqliteData(item);
52
115
  this.castToBoolean(item);
53
116
  await this.prepareItem(item);
54
117
  }
@@ -57,11 +120,6 @@ class AbstractSqliteRepository {
57
120
  if (!data[this.identifier]) {
58
121
  data[this.identifier] = randomUUID();
59
122
  }
60
- for (const key in data) {
61
- if (typeof data[key] === 'boolean') {
62
- data[key] = data[key] ? 1 : 0;
63
- }
64
- }
65
123
  if (this.hasCreatedAt()) {
66
124
  data.createdAt = (new Date().toISOString());
67
125
  }
@@ -69,6 +127,7 @@ class AbstractSqliteRepository {
69
127
  data.updatedAt = (new Date().toISOString());
70
128
  }
71
129
  await this.prepareData(data);
130
+ this.normalizeSqliteData(data);
72
131
  const fields = Object.keys(data)
73
132
  .map(field => `${field}`)
74
133
  .join(', ');
@@ -82,7 +141,7 @@ class AbstractSqliteRepository {
82
141
  return item;
83
142
  }
84
143
  catch (e) {
85
- console.error(e);
144
+ console.error("sqlite create", e);
86
145
  throw SqliteErrorToValidationError(e, data);
87
146
  }
88
147
  }
@@ -91,15 +150,11 @@ class AbstractSqliteRepository {
91
150
  }
92
151
  async update(id, data) {
93
152
  try {
94
- for (const key in data) {
95
- if (typeof data[key] === 'boolean') {
96
- data[key] = data[key] ? 1 : 0;
97
- }
98
- }
99
153
  if (this.hasUpdatedAt()) {
100
154
  data.updatedAt = (new Date().toISOString());
101
155
  }
102
156
  await this.prepareData(data);
157
+ this.normalizeSqliteData(data);
103
158
  const setClauses = Object.keys(data)
104
159
  .map(field => `${field} = @${field}`)
105
160
  .join(', ');
@@ -159,12 +214,14 @@ class AbstractSqliteRepository {
159
214
  for (const item of items) {
160
215
  await this.decorate(item);
161
216
  }
162
- return {
217
+ const pagination = {
163
218
  page,
164
219
  limit,
165
220
  total: rCount.count,
166
221
  items
167
222
  };
223
+ console.log('Pagination result:', JSON.stringify(pagination, null, 4));
224
+ return pagination;
168
225
  }
169
226
  async find({ limit = 5, orderBy = '', order = 'desc', search = '', filters = [] }) {
170
227
  let where = "";
@@ -219,6 +276,9 @@ class AbstractSqliteRepository {
219
276
  return items;
220
277
  }
221
278
  async findById(id) {
279
+ if (id === undefined || id === null) {
280
+ return null;
281
+ }
222
282
  const item = this.db.prepare(`SELECT *
223
283
  FROM ${this.tableName}
224
284
  WHERE ${this.identifier} = ?`).get(id);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "3.1.0",
6
+ "version": "3.4.1",
7
7
  "description": "Crud utils across modules",
8
8
  "main": "dist/index.js",
9
9
  "types": "types/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  "@drax/common-back": "^3.0.0",
26
26
  "@drax/common-share": "^3.0.0",
27
27
  "@drax/identity-share": "^3.0.0",
28
- "@drax/media-back": "^3.1.0",
28
+ "@drax/media-back": "^3.4.1",
29
29
  "@graphql-tools/load-files": "^7.0.0",
30
30
  "@graphql-tools/merge": "^9.0.4",
31
31
  "mongoose": "^8.23.0",
@@ -47,5 +47,5 @@
47
47
  "typescript": "^5.9.3",
48
48
  "vitest": "^3.2.4"
49
49
  },
50
- "gitHead": "262ea8f861c84ca1ad4d555545c5a94b95dbf25e"
50
+ "gitHead": "bd93239e7b1cc56f1a12d9cb28ea5d87cda48c07"
51
51
  }
@@ -23,6 +23,7 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
23
23
  protected tableName: string = '';
24
24
  protected searchFields: string[] = [];
25
25
  protected booleanFields: string[] = [];
26
+ protected jsonFields: string[] = [];
26
27
  protected identifier: string = '_id';
27
28
  protected populateFields: { field: string, table: string, identifier: string }[]
28
29
  protected tableFields: SqliteTableField[];
@@ -59,6 +60,84 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
59
60
  return item
60
61
  }
61
62
 
63
+ protected parseJsonValue(value: any): any {
64
+ if (typeof value !== 'string') {
65
+ return value
66
+ }
67
+
68
+ try {
69
+ return JSON.parse(value)
70
+ } catch {
71
+ return value
72
+ }
73
+ }
74
+
75
+ protected normalizeSqliteValue(value: any): any {
76
+ if (value === undefined) {
77
+ return null
78
+ }
79
+
80
+ if (value === null) {
81
+ return null
82
+ }
83
+
84
+ if (typeof value === 'boolean') {
85
+ return value ? 1 : 0
86
+ }
87
+
88
+ if (typeof value === 'number' || typeof value === 'string' || typeof value === 'bigint') {
89
+ return value
90
+ }
91
+
92
+ if (Buffer.isBuffer(value)) {
93
+ return value
94
+ }
95
+
96
+ if (value instanceof Date) {
97
+ return value.toISOString()
98
+ }
99
+
100
+ if (value instanceof ArrayBuffer) {
101
+ return Buffer.from(value)
102
+ }
103
+
104
+ if (ArrayBuffer.isView(value)) {
105
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength)
106
+ }
107
+
108
+ if (typeof value === 'object') {
109
+ return JSON.stringify(value)
110
+ }
111
+
112
+ return String(value)
113
+ }
114
+
115
+ protected normalizeSqliteData(data: any): any {
116
+ if (!data || typeof data !== 'object') {
117
+ return data
118
+ }
119
+
120
+ for (const key of Object.keys(data)) {
121
+ data[key] = this.normalizeSqliteValue(data[key])
122
+ }
123
+
124
+ return data
125
+ }
126
+
127
+ protected deserializeSqliteData(item: any): any {
128
+ if (!item || typeof item !== 'object') {
129
+ return item
130
+ }
131
+
132
+ for (const field of this.jsonFields) {
133
+ if (Object.prototype.hasOwnProperty.call(item, field)) {
134
+ item[field] = this.parseJsonValue(item[field])
135
+ }
136
+ }
137
+
138
+ return item
139
+ }
140
+
62
141
  async execPopulate(item: any) {
63
142
  for (const field of this.populateFields) {
64
143
  if (item[field.field]) {
@@ -81,10 +160,13 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
81
160
 
82
161
  async decorate(item: any) {
83
162
  await this.execPopulate(item)
163
+ this.deserializeSqliteData(item)
84
164
  this.castToBoolean(item)
85
165
  await this.prepareItem(item)
86
166
  }
87
167
 
168
+
169
+
88
170
  async create(data: any): Promise<T> {
89
171
  try {
90
172
 
@@ -92,12 +174,6 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
92
174
  data[this.identifier] = randomUUID()
93
175
  }
94
176
 
95
- for (const key in data) {
96
- if (typeof data[key] === 'boolean') {
97
- data[key] = data[key] ? 1 : 0
98
- }
99
- }
100
-
101
177
  if (this.hasCreatedAt()) {
102
178
  data.createdAt = (new Date().toISOString())
103
179
  }
@@ -106,7 +182,10 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
106
182
  data.updatedAt = (new Date().toISOString())
107
183
  }
108
184
 
185
+
186
+
109
187
  await this.prepareData(data)
188
+ this.normalizeSqliteData(data)
110
189
 
111
190
  const fields = Object.keys(data)
112
191
  .map(field => `${field}`)
@@ -124,7 +203,7 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
124
203
  return item
125
204
 
126
205
  } catch (e) {
127
- console.error(e)
206
+ console.error("sqlite create",e)
128
207
  throw SqliteErrorToValidationError(e, data)
129
208
  }
130
209
  }
@@ -137,17 +216,12 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
137
216
  async update(id: string, data: any): Promise<T> {
138
217
  try {
139
218
 
140
- for (const key in data) {
141
- if (typeof data[key] === 'boolean') {
142
- data[key] = data[key] ? 1 : 0
143
- }
144
- }
145
-
146
219
  if (this.hasUpdatedAt()) {
147
220
  data.updatedAt = (new Date().toISOString())
148
221
  }
149
222
 
150
223
  await this.prepareData(data)
224
+ this.normalizeSqliteData(data)
151
225
 
152
226
  const setClauses = Object.keys(data)
153
227
  .map(field => `${field} = @${field}`)
@@ -237,12 +311,14 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
237
311
  await this.decorate(item)
238
312
  }
239
313
 
240
- return {
314
+ const pagination = {
241
315
  page,
242
316
  limit,
243
317
  total: rCount.count,
244
318
  items
245
319
  }
320
+ console.log('Pagination result:', JSON.stringify(pagination,null,4))
321
+ return pagination
246
322
  }
247
323
 
248
324
  async find({
@@ -327,6 +403,10 @@ class AbstractSqliteRepository<T, C, U> implements IDraxCrud<T, C, U> {
327
403
  }
328
404
 
329
405
  async findById(id: string): Promise<T | null> {
406
+ if (id === undefined || id === null) {
407
+ return null
408
+ }
409
+
330
410
  const item = this.db.prepare(`SELECT *
331
411
  FROM ${this.tableName}
332
412
  WHERE ${this.identifier} = ?`).get(id);
@@ -11,6 +11,7 @@ class LanguageSqliteRepository extends AbstractSqliteRepository<ILanguage, ILang
11
11
  protected dataBaseFile: string;
12
12
  protected searchFields: string[] = ['name'];
13
13
  protected booleanFields: string[] = [];
14
+ protected jsonFields: string[] = ['icon'];
14
15
  protected identifier: string = '_id';
15
16
  protected populateFields = [
16
17
 
@@ -25,13 +26,7 @@ class LanguageSqliteRepository extends AbstractSqliteRepository<ILanguage, ILang
25
26
  async prepareData(data: any): Promise<any> {
26
27
  data.icon = JSON.stringify(data.icon)
27
28
  }
28
-
29
- async prepareItem(item: any): Promise<any> {
30
- item.icon = JSON.parse(item.icon)
31
- }
32
-
33
29
  }
34
30
 
35
31
  export default LanguageSqliteRepository
36
32
  export {LanguageSqliteRepository}
37
-
@@ -0,0 +1,95 @@
1
+ import {describe, test} from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {randomUUID} from "node:crypto";
4
+ import {AbstractSqliteRepository} from "../../src/index.js";
5
+ import type {SqliteTableField} from "@drax/common-back";
6
+
7
+ class TestSqliteRepository extends AbstractSqliteRepository<any, any, any> {
8
+ protected tableName: string = "TestEntity";
9
+ protected searchFields: string[] = [];
10
+ protected booleanFields: string[] = ["enabled"];
11
+ protected jsonFields: string[] = ["payload", "tags"];
12
+ protected identifier: string = "_id";
13
+ protected populateFields = [];
14
+ protected tableFields: SqliteTableField[] = [
15
+ {name: "title", type: "TEXT", unique: false, primary: false},
16
+ {name: "payload", type: "TEXT", unique: false, primary: false},
17
+ {name: "tags", type: "TEXT", unique: false, primary: false},
18
+ {name: "enabled", type: "INTEGER", unique: false, primary: false},
19
+ {name: "bornAt", type: "TEXT", unique: false, primary: false},
20
+ {name: "optional", type: "TEXT", unique: false, primary: false},
21
+ {name: "createdAt", type: "TEXT", unique: false, primary: false},
22
+ {name: "updatedAt", type: "TEXT", unique: false, primary: false}
23
+ ];
24
+
25
+ findRawById(id: string) {
26
+ return this.db.prepare(`SELECT * FROM ${this.tableName} WHERE ${this.identifier} = ?`).get(id);
27
+ }
28
+ }
29
+
30
+ describe("AbstractSqliteRepository", () => {
31
+ test("create should normalize non-bindable sqlite values", async () => {
32
+ const repository = new TestSqliteRepository(`/tmp/${randomUUID()}.db`);
33
+ repository.build();
34
+
35
+ const bornAt = new Date("2024-01-02T03:04:05.000Z");
36
+
37
+ const created = await repository.create({
38
+ title: "example",
39
+ payload: {foo: "bar", count: 2},
40
+ tags: ["a", "b"],
41
+ enabled: true,
42
+ bornAt,
43
+ optional: undefined
44
+ });
45
+
46
+ const raw = repository.findRawById(created._id);
47
+
48
+ assert.deepEqual(created.payload, {foo: "bar", count: 2});
49
+ assert.deepEqual(created.tags, ["a", "b"]);
50
+ assert.equal(created.enabled, true);
51
+ assert.equal(created.bornAt, bornAt.toISOString());
52
+ assert.equal(created.optional, null);
53
+ assert.equal(raw.title, "example");
54
+ assert.equal(raw.payload, JSON.stringify({foo: "bar", count: 2}));
55
+ assert.equal(raw.tags, JSON.stringify(["a", "b"]));
56
+ assert.equal(raw.enabled, 1);
57
+ assert.equal(raw.bornAt, bornAt.toISOString());
58
+ assert.equal(raw.optional, null);
59
+ assert.ok(typeof raw.createdAt === "string");
60
+ assert.ok(typeof raw.updatedAt === "string");
61
+ });
62
+
63
+ test("update should normalize non-bindable sqlite values", async () => {
64
+ const repository = new TestSqliteRepository(`/tmp/${randomUUID()}.db`);
65
+ repository.build();
66
+
67
+ const created = await repository.create({
68
+ title: "initial"
69
+ });
70
+
71
+ const bornAt = new Date("2025-06-07T08:09:10.000Z");
72
+
73
+ const updated = await repository.update(created._id, {
74
+ payload: {nested: ["x"]},
75
+ tags: ["updated"],
76
+ enabled: false,
77
+ bornAt,
78
+ optional: undefined
79
+ });
80
+
81
+ const raw = repository.findRawById(created._id);
82
+
83
+ assert.deepEqual(updated.payload, {nested: ["x"]});
84
+ assert.deepEqual(updated.tags, ["updated"]);
85
+ assert.equal(updated.enabled, false);
86
+ assert.equal(updated.bornAt, bornAt.toISOString());
87
+ assert.equal(updated.optional, null);
88
+ assert.equal(raw.payload, JSON.stringify({nested: ["x"]}));
89
+ assert.equal(raw.tags, JSON.stringify(["updated"]));
90
+ assert.equal(raw.enabled, 0);
91
+ assert.equal(raw.bornAt, bornAt.toISOString());
92
+ assert.equal(raw.optional, null);
93
+ assert.ok(typeof raw.updatedAt === "string");
94
+ });
95
+ });