json-rest-api 1.0.5 → 1.0.6

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.
@@ -56,7 +56,7 @@ The API validates and normalizes date/time inputs:
56
56
  "type": "articles",
57
57
  "attributes": {
58
58
  "publishedDate": "2024-01-15", // Valid date
59
- "createdAt": "2024-01-15 14:30:00", // Valid dateTime
59
+ "createdAt": "2024-01-15T14:30:00Z", // Valid dateTime (ISO 8601)
60
60
  "dailyPostTime": "14:30:00" // Valid time
61
61
  }
62
62
  }
@@ -64,12 +64,21 @@ The API validates and normalizes date/time inputs:
64
64
  ```
65
65
 
66
66
  **Accepted Input Formats:**
67
- - **date**: `YYYY-MM-DD`
67
+ - **date**:
68
+ - `YYYY-MM-DD` (parsed at UTC midnight)
69
+ - ISO 8601 date strings
70
+ - Any JavaScript Date parseable string
68
71
  - **dateTime**:
69
- - `YYYY-MM-DD HH:MM:SS`
70
- - ISO 8601 strings (`2024-01-15T14:30:00Z`)
72
+ - ISO 8601 strings (`2024-01-15T14:30:00Z`) - recommended
73
+ - `YYYY-MM-DD HH:MM:SS` (assumed UTC)
71
74
  - JavaScript Date parseable strings
72
- - **time**: `HH:MM:SS` or `HH:MM`
75
+ - Unix timestamps (as numbers)
76
+ - **time**:
77
+ - `HH:MM:SS` or `HH:MM`
78
+ - Extracted from datetime strings
79
+
80
+ **Storage Format:**
81
+ All date/time values are converted to JavaScript Date objects before storage, allowing the database driver to handle the appropriate formatting for each database system.
73
82
 
74
83
  ## Output Normalization
75
84
 
@@ -206,11 +215,14 @@ The date/time handling is implemented in two key areas:
206
215
 
207
216
  1. **Input Validation** (`json-rest-schema`):
208
217
  - Validates format on write operations
209
- - Converts to standard storage format
218
+ - Converts all date inputs to JavaScript Date objects
219
+ - Ensures date-only values parse at UTC midnight
220
+ - Returns Date objects for storage (Knex handles DB-specific formatting)
210
221
 
211
222
  2. **Output Normalization** (`database-value-normalizers.js`):
212
- - Converts database values to JavaScript Date objects
213
- - Handles database-specific quirks
214
- - Ensures consistent API output
223
+ - Handles database-specific quirks (MySQL timezone issues)
224
+ - Ensures Date objects are properly created from database values
225
+ - Fixes MySQL datetime strings by assuming UTC
226
+ - Maintains consistency across different database engines
215
227
 
216
- This two-stage approach ensures data integrity on input and consistent formatting on output, regardless of the underlying database system.
228
+ This two-stage approach ensures data integrity on input and consistent formatting on output, regardless of the underlying database system. The key insight is that JavaScript Date objects are used as the common format throughout the pipeline, with database drivers handling the conversion to/from their native formats.
package/example.js CHANGED
@@ -8,7 +8,7 @@ import util from 'util';
8
8
 
9
9
  // Create a Knex instance connected to SQLite in-memory database
10
10
  const knex = knexLib({
11
- client: 'sqlite3',
11
+ client: 'better-sqlite3',
12
12
  connection: {
13
13
  filename: ':memory:'
14
14
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-rest-api",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,44 +22,71 @@
22
22
  "homepage": "https://github.com/mobily-enterprises/jsonrestapi#readme",
23
23
  "description": "REST API plugin for hooked-api with JSON:API compliance",
24
24
  "dependencies": {
25
- "@socket.io/redis-adapter": "^8.3.0",
26
- "fractional-indexing": "^3.2.0",
27
25
  "hooked-api": "^1.0.8",
28
- "json-rest-schema": "^1.0.6",
29
- "jsonwebtoken": "^9.0.2",
30
- "jwks-rsa": "^3.2.0",
31
- "knex": "^3.1.0",
32
- "qs": "^6.0.0",
33
- "redis": "^5.6.0",
34
- "socket.io": "^4.8.1",
35
- "sqlite3": "^5.1.7"
26
+ "json-rest-schema": "^1.0.6"
36
27
  },
37
28
  "peerDependencies": {
29
+ "@socket.io/redis-adapter": "^8.3.0",
38
30
  "busboy": "^1.0.0",
39
31
  "content-type": "^1.0.0",
40
32
  "express": "^4.0.0 || ^5.0.0",
41
33
  "formidable": "^3.0.0",
42
- "raw-body": "^2.0.0"
34
+ "fractional-indexing": "^3.2.0",
35
+ "jsonwebtoken": "^9.0.2",
36
+ "jwks-rsa": "^3.2.0",
37
+ "knex": "^3.1.0",
38
+ "raw-body": "^2.0.0",
39
+ "redis": "^5.6.0",
40
+ "socket.io": "^4.8.1"
43
41
  },
44
42
  "peerDependenciesMeta": {
45
- "express": {
43
+ "@socket.io/redis-adapter": {
44
+ "optional": true
45
+ },
46
+ "busboy": {
46
47
  "optional": true
47
48
  },
48
49
  "content-type": {
49
50
  "optional": true
50
51
  },
52
+ "express": {
53
+ "optional": true
54
+ },
55
+ "formidable": {
56
+ "optional": true
57
+ },
58
+ "fractional-indexing": {
59
+ "optional": true
60
+ },
61
+ "jsonwebtoken": {
62
+ "optional": true
63
+ },
64
+ "jwks-rsa": {
65
+ "optional": true
66
+ },
67
+ "knex": {
68
+ "optional": true
69
+ },
51
70
  "raw-body": {
52
71
  "optional": true
53
72
  },
54
- "busboy": {
73
+ "redis": {
55
74
  "optional": true
56
75
  },
57
- "formidable": {
76
+ "socket.io": {
58
77
  "optional": true
59
78
  }
60
79
  },
61
80
  "devDependencies": {
81
+ "@socket.io/redis-adapter": "^8.3.0",
82
+ "better-sqlite3": "^11.0.0",
62
83
  "express": "^5.1.0",
84
+ "fractional-indexing": "^3.2.0",
85
+ "jsonwebtoken": "^9.0.2",
86
+ "jwks-rsa": "^3.2.0",
87
+ "knex": "^3.1.0",
88
+ "redis": "^5.6.0",
89
+ "socket.io": "^4.8.1",
63
90
  "socket.io-client": "^4.8.1",
64
91
  "supertest": "^7.1.3"
65
92
  }
@@ -23,9 +23,17 @@
23
23
  * ```
24
24
  */
25
25
 
26
- import Busboy from 'busboy';
26
+ import { requirePackage } from 'hooked-api';
27
27
  import { Readable } from 'stream';
28
28
 
29
+ let Busboy;
30
+ try {
31
+ Busboy = (await import('busboy')).default;
32
+ } catch (e) {
33
+ requirePackage('busboy', 'express/http-connector',
34
+ 'Busboy is required for multipart/form-data file uploads. This is a peer dependency.');
35
+ }
36
+
29
37
  /**
30
38
  * Creates a busboy-based file detector
31
39
  *
@@ -26,10 +26,18 @@
26
26
  * ```
27
27
  */
28
28
 
29
- import formidable from 'formidable';
29
+ import { requirePackage } from 'hooked-api';
30
30
  import { promises as fs } from 'fs';
31
31
  import path from 'path';
32
32
 
33
+ let formidable;
34
+ try {
35
+ formidable = (await import('formidable')).default;
36
+ } catch (e) {
37
+ requirePackage('formidable', 'express/http-connector',
38
+ 'Formidable is required for multipart/form-data file uploads with disk storage. This is a peer dependency.');
39
+ }
40
+
33
41
  /**
34
42
  * Creates a formidable-based file detector
35
43
  *
@@ -15,7 +15,7 @@
15
15
  * - Providing helpers for programmatic auth checks
16
16
  */
17
17
 
18
- import jwksClient from 'jwks-rsa';
18
+ import { requirePackage } from 'hooked-api';
19
19
  import {
20
20
  verifyToken,
21
21
  createRevocationResource,
@@ -141,6 +141,15 @@ export const JwtAuthPlugin = {
141
141
 
142
142
  let jwksClientInstance;
143
143
  if (config.jwksUrl) {
144
+ // Dynamic import for jwks-rsa
145
+ let jwksClient;
146
+ try {
147
+ jwksClient = (await import('jwks-rsa')).default;
148
+ } catch (e) {
149
+ requirePackage('jwks-rsa', 'jwt-auth',
150
+ 'JWKS RSA support is required for dynamic key rotation with providers like Auth0, Supabase, etc. This is a peer dependency.');
151
+ }
152
+
144
153
  jwksClientInstance = jwksClient({
145
154
  jwksUri: config.jwksUrl,
146
155
  cache: true, // Cache keys to reduce network calls
@@ -1,4 +1,12 @@
1
- import { generateKeyBetween } from 'fractional-indexing';
1
+ import { requirePackage } from 'hooked-api';
2
+
3
+ let generateKeyBetween;
4
+ try {
5
+ ({ generateKeyBetween } = await import('fractional-indexing'));
6
+ } catch (e) {
7
+ requirePackage('fractional-indexing', 'positioning',
8
+ 'Fractional indexing is required for the positioning plugin to generate sortable position keys. This is a peer dependency.');
9
+ }
2
10
 
3
11
  /**
4
12
  * Helper functions for fractional positioning
@@ -1,4 +1,12 @@
1
- import jwt from 'jsonwebtoken';
1
+ import { requirePackage } from 'hooked-api';
2
+
3
+ let jwt;
4
+ try {
5
+ jwt = (await import('jsonwebtoken')).default;
6
+ } catch (e) {
7
+ requirePackage('jsonwebtoken', 'jwt-auth',
8
+ 'JSON Web Token support is required for JWT authentication. This is a peer dependency.');
9
+ }
2
10
 
3
11
  /**
4
12
  * Verify JWT token with multiple strategies
@@ -1,3 +1,4 @@
1
+ import { requirePackage } from 'hooked-api';
1
2
  import { createSchema, createKnexTable } from 'json-rest-schema';
2
3
  import { createCrossTableSearchHelpers } from './lib/knex-cross-table-search.js';
3
4
  import { getForeignKeyFields, buildFieldSelection, isNonDatabaseField } from './lib/knex-field-helpers.js';
@@ -42,12 +43,17 @@ export const RestApiKnexPlugin = {
42
43
 
43
44
  async install({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
44
45
 
45
- // Get Knex instance from plugin options
46
- const knexOptions = pluginOptions || {};
47
- if (!knexOptions.knex) {
48
- throw new Error('RestApiKnexPlugin requires a knex instance');
46
+ // Try to import knex dynamically
47
+ let knexLib;
48
+ try {
49
+ knexLib = await import('knex');
50
+ } catch (e) {
51
+ requirePackage('knex', 'rest-api-knex',
52
+ 'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.');
49
53
  }
50
54
 
55
+ // Get Knex instance from plugin options
56
+ const knexOptions = pluginOptions || {};
51
57
  const knex = knexOptions.knex;
52
58
 
53
59
  // Expose Knex instance and helpers in a structured way
@@ -1,6 +1,4 @@
1
- import { Server } from 'socket.io';
2
- import { createAdapter } from '@socket.io/redis-adapter';
3
- import { createClient } from 'redis';
1
+ import { requirePackage } from 'hooked-api';
4
2
 
5
3
  // WeakMap to store pending broadcasts per transaction
6
4
  const pendingBroadcasts = new WeakMap();
@@ -10,6 +8,16 @@ export const SocketIOPlugin = {
10
8
  dependencies: ['rest-api', 'jwt-auth'],
11
9
 
12
10
  async install({ api, addHook, log, scopes, helpers, vars, runHooks }) {
11
+ // Dynamic imports for socket.io and related packages
12
+ let Server, createAdapter, createClient;
13
+
14
+ try {
15
+ ({ Server } = await import('socket.io'));
16
+ } catch (e) {
17
+ requirePackage('socket.io', 'socketio',
18
+ 'Socket.IO is required for WebSocket support. This is a peer dependency.');
19
+ }
20
+
13
21
  let io;
14
22
 
15
23
  // Helper function to match filters using searchSchema
@@ -221,6 +229,21 @@ export const SocketIOPlugin = {
221
229
 
222
230
  // Set up Redis adapter if configured
223
231
  if (redis) {
232
+ // Dynamic import for Redis dependencies
233
+ try {
234
+ ({ createClient } = await import('redis'));
235
+ } catch (e) {
236
+ requirePackage('redis', 'socketio',
237
+ 'Redis is required for Socket.IO horizontal scaling. This is a peer dependency.');
238
+ }
239
+
240
+ try {
241
+ ({ createAdapter } = await import('@socket.io/redis-adapter'));
242
+ } catch (e) {
243
+ requirePackage('@socket.io/redis-adapter', 'socketio',
244
+ 'Socket.IO Redis adapter is required for horizontal scaling. This is a peer dependency.');
245
+ }
246
+
224
247
  const pubClient = createClient(redis);
225
248
  const subClient = pubClient.duplicate();
226
249
 
@@ -68,7 +68,7 @@ import { createBasicApi } from './fixtures/api-configs.js';
68
68
 
69
69
  // Create Knex instance for tests - always use SQLite in-memory
70
70
  const knex = knexLib({
71
- client: 'sqlite3',
71
+ client: 'better-sqlite3',
72
72
  connection: {
73
73
  filename: ':memory:'
74
74
  },
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  // Create Knex instance for tests
15
15
  const knex = knexLib({
16
- client: 'sqlite3',
16
+ client: 'better-sqlite3',
17
17
  connection: {
18
18
  filename: ':memory:'
19
19
  },
@@ -14,7 +14,7 @@ import { CorsPlugin } from '../plugins/core/rest-api-cors-plugin.js';
14
14
 
15
15
  // Create Knex instance for tests - always use SQLite in-memory
16
16
  const knex = knexLib({
17
- client: 'sqlite3',
17
+ client: 'better-sqlite3',
18
18
  connection: {
19
19
  filename: ':memory:'
20
20
  },
@@ -16,7 +16,7 @@ import http from 'http';
16
16
 
17
17
  // Create Knex instance for tests
18
18
  const knex = knexLib({
19
- client: 'sqlite3',
19
+ client: 'better-sqlite3',
20
20
  connection: {
21
21
  filename: ':memory:'
22
22
  },
@@ -6,7 +6,7 @@ import { cleanTables } from './helpers/test-utils.js';
6
6
 
7
7
  // Create Knex instance for tests
8
8
  const knex = knexLib({
9
- client: 'sqlite3',
9
+ client: 'better-sqlite3',
10
10
  connection: {
11
11
  filename: ':memory:'
12
12
  },
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  // Create Knex instance for tests
18
18
  const knex = knexLib({
19
- client: 'sqlite3',
19
+ client: 'better-sqlite3',
20
20
  connection: {
21
21
  filename: ':memory:'
22
22
  },
@@ -39,7 +39,7 @@ function createToken(payload, options = {}) {
39
39
 
40
40
  // Create Knex instance for tests
41
41
  const knex = knexLib({
42
- client: 'sqlite3',
42
+ client: 'better-sqlite3',
43
43
  connection: {
44
44
  filename: ':memory:'
45
45
  },
@@ -13,7 +13,7 @@ import { createMultiHomeApi } from './fixtures/api-configs.js';
13
13
 
14
14
  // Create Knex instance for tests - always use SQLite in-memory
15
15
  const knex = knexLib({
16
- client: 'sqlite3',
16
+ client: 'better-sqlite3',
17
17
  connection: {
18
18
  filename: ':memory:'
19
19
  },
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  // Create Knex instance for tests
14
14
  const knex = knexLib({
15
- client: 'sqlite3',
15
+ client: 'better-sqlite3',
16
16
  connection: {
17
17
  filename: ':memory:'
18
18
  },
@@ -12,7 +12,7 @@ import { createPaginationApi } from './fixtures/api-configs.js';
12
12
 
13
13
  // Create Knex instance for tests
14
14
  const knex = knexLib({
15
- client: 'sqlite3',
15
+ client: 'better-sqlite3',
16
16
  connection: {
17
17
  filename: ':memory:'
18
18
  },
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  // Create Knex instance for tests
13
13
  const knex = knexLib({
14
- client: 'sqlite3',
14
+ client: 'better-sqlite3',
15
15
  connection: {
16
16
  filename: ':memory:'
17
17
  },
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  // Create Knex instance for tests
13
13
  const knex = knexLib({
14
- client: 'sqlite3',
14
+ client: 'better-sqlite3',
15
15
  connection: {
16
16
  filename: ':memory:'
17
17
  },
@@ -8,7 +8,7 @@ import { rebalancePositions, isValidPosition, getUnpositionedItems, assignInitia
8
8
 
9
9
  // Create Knex instance for tests
10
10
  const knex = knexLib({
11
- client: 'sqlite3',
11
+ client: 'better-sqlite3',
12
12
  connection: {
13
13
  filename: ':memory:'
14
14
  },
@@ -7,7 +7,7 @@ import { PositioningPlugin } from '../plugins/core/rest-api-positioning-plugin.j
7
7
 
8
8
  // Create Knex instance for tests
9
9
  const knex = knexLib({
10
- client: 'sqlite3',
10
+ client: 'better-sqlite3',
11
11
  connection: {
12
12
  filename: ':memory:'
13
13
  },
@@ -7,7 +7,7 @@ import { PositioningPlugin } from '../plugins/core/rest-api-positioning-plugin.j
7
7
 
8
8
  // Create Knex instance for tests
9
9
  const knex = knexLib({
10
- client: 'sqlite3',
10
+ client: 'better-sqlite3',
11
11
  connection: {
12
12
  filename: ':memory:'
13
13
  },
@@ -7,7 +7,7 @@ import { PositioningPlugin } from '../plugins/core/rest-api-positioning-plugin.j
7
7
 
8
8
  // Create Knex instance for tests - always use SQLite in-memory
9
9
  const knex = knexLib({
10
- client: 'sqlite3',
10
+ client: 'better-sqlite3',
11
11
  connection: {
12
12
  filename: ':memory:'
13
13
  },
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  // Create Knex instance for tests
18
18
  const knex = knexLib({
19
- client: 'sqlite3',
19
+ client: 'better-sqlite3',
20
20
  connection: {
21
21
  filename: ':memory:'
22
22
  },
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  // Create Knex instance for tests
15
15
  const knex = knexLib({
16
- client: 'sqlite3',
16
+ client: 'better-sqlite3',
17
17
  connection: {
18
18
  filename: ':memory:'
19
19
  },
@@ -15,7 +15,7 @@ import { RestApiRelationshipsPlugin } from '../plugins/core/rest-api-relationshi
15
15
 
16
16
  // Create Knex instance for tests
17
17
  const knex = knexLib({
18
- client: 'sqlite3',
18
+ client: 'better-sqlite3',
19
19
  connection: {
20
20
  filename: ':memory:'
21
21
  },
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  // Create Knex instance for tests
18
18
  const knex = knexLib({
19
- client: 'sqlite3',
19
+ client: 'better-sqlite3',
20
20
  connection: {
21
21
  filename: ':memory:'
22
22
  },
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  // Create Knex instance for tests
18
18
  const knex = knexLib({
19
- client: 'sqlite3',
19
+ client: 'better-sqlite3',
20
20
  connection: {
21
21
  filename: ':memory:'
22
22
  },
@@ -15,7 +15,7 @@ import { createWebSocketApi } from './fixtures/api-configs.js';
15
15
 
16
16
  // Create Knex instance for tests
17
17
  const knex = knexLib({
18
- client: 'sqlite3',
18
+ client: 'better-sqlite3',
19
19
  connection: {
20
20
  filename: ':memory:'
21
21
  },
@@ -13,7 +13,7 @@ import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js';
13
13
 
14
14
  // Create Knex instance for tests - always use SQLite in-memory
15
15
  const knex = knexLib({
16
- client: 'sqlite3',
16
+ client: 'better-sqlite3',
17
17
  connection: {
18
18
  filename: ':memory:'
19
19
  },