json-rest-api 1.0.23 → 1.0.25

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.
Files changed (106) hide show
  1. package/README.md +24 -2
  2. package/docs/API.md +157 -33
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
  10. package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
  11. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  12. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  13. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  14. package/docs/GUIDE/index.md +4 -1
  15. package/fixing_plan.md +112 -0
  16. package/index.js +2 -0
  17. package/lib/enhanced-logger.js +1 -1
  18. package/package.json +6 -2
  19. package/plugins/core/bulk-operations-plugin.js +31 -38
  20. package/plugins/core/connectors/express-plugin.js +6 -7
  21. package/plugins/core/connectors/fastify-plugin.js +5 -1
  22. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  23. package/plugins/core/connectors/lib/connector-core.js +17 -11
  24. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  25. package/plugins/core/file-handling-plugin.js +126 -96
  26. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  27. package/plugins/core/lib/anyapi/schema-utils.js +44 -4
  28. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
  29. package/plugins/core/lib/dbIntrospection.js +1 -1
  30. package/plugins/core/lib/dbTablesOperations.js +10 -3
  31. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  32. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
  33. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  34. package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
  35. package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
  36. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  37. package/plugins/core/lib/querying/url-helpers.js +9 -21
  38. package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
  39. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  40. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  41. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  42. package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
  43. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  44. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  45. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  46. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  47. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
  48. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  49. package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
  50. package/plugins/core/lib/storage/storage-adapter.js +76 -14
  51. package/plugins/core/lib/storage/storage-mapping.js +86 -16
  52. package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
  53. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  54. package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
  55. package/plugins/core/rest-api-knex-plugin.js +93 -76
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
  57. package/plugins/core/rest-api-plugin-methods/common.js +112 -72
  58. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
  59. package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
  60. package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
  61. package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
  62. package/plugins/core/rest-api-plugin-methods/get.js +7 -1
  63. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
  64. package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
  65. package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
  66. package/plugins/core/rest-api-plugin-methods/post.js +12 -7
  67. package/plugins/core/rest-api-plugin-methods/put.js +35 -10
  68. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  69. package/plugins/core/rest-api-plugin.js +11 -37
  70. package/plugins/core/rest-api-positioning-plugin.js +1 -4
  71. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  72. package/plugins/core/socketio-plugin.js +0 -1
  73. package/plugins/storage/local-storage.js +45 -20
  74. package/plugins/storage/s3-storage.js +23 -54
  75. package/quickTest.js +1 -0
  76. package/scripts/run-docs.js +80 -84
  77. package/test-debug.js +1 -1
  78. package/tests/README.md +8 -1
  79. package/tests/anyapi-basic.test.js +3 -0
  80. package/tests/anyapi-cursor-pagination.test.js +7 -1
  81. package/tests/anyapi-custom-idproperty.test.js +163 -0
  82. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  83. package/tests/db-introspection.test.js +4 -1
  84. package/tests/db-table-operations.test.js +25 -20
  85. package/tests/fastify-plugin.test.js +49 -6
  86. package/tests/field-setters.test.js +10 -10
  87. package/tests/file-handling.test.js +118 -0
  88. package/tests/fixtures/api-configs.js +495 -4
  89. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  90. package/tests/http-connectors-parity.test.js +165 -5
  91. package/tests/id-normalization.test.js +444 -0
  92. package/tests/local-storage.test.js +96 -0
  93. package/tests/logical-id-write-validation.test.js +151 -0
  94. package/tests/pagination-cursor-multifield.test.js +132 -11
  95. package/tests/pagination-enhanced.test.js +66 -15
  96. package/tests/post-commit-hooks.test.js +120 -0
  97. package/tests/public-exports.test.js +15 -0
  98. package/tests/put-create-parity.test.js +216 -0
  99. package/tests/relationship-endpoints.test.js +259 -0
  100. package/tests/return-record-settings.test.js +184 -0
  101. package/tests/row-policy.test.js +433 -0
  102. package/tests/s3-storage.test.js +38 -0
  103. package/tests/searchschema-merge.test.js +17 -0
  104. package/tests/socketio.test.js +62 -0
  105. package/tests/storage-mapping.test.js +56 -6
  106. package/tests/test-development-guide.md +5 -5
@@ -1,5 +1,8 @@
1
1
  const EMPTY_STORAGE_INFO = Object.freeze({
2
2
  idColumn: 'id',
3
+ storage: Object.freeze({
4
+ naming: 'snake_case'
5
+ }),
3
6
  fields: Object.freeze({}),
4
7
  columns: Object.freeze({})
5
8
  })
@@ -10,14 +13,75 @@ const isPlainObject = (value) => (
10
13
  !Array.isArray(value)
11
14
  )
12
15
 
13
- export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = 'id' } = {}) => {
14
- const storage = definition.storage
16
+ const SNAKE_CASE_STORAGE_NAMING = 'snake_case'
17
+ const EXACT_STORAGE_NAMING = 'exact'
15
18
 
16
- if (storage !== undefined && !isPlainObject(storage)) {
19
+ const STORAGE_NAMING_ALIASES = Object.freeze({
20
+ snake: SNAKE_CASE_STORAGE_NAMING,
21
+ snake_case: SNAKE_CASE_STORAGE_NAMING,
22
+ snakeCase: SNAKE_CASE_STORAGE_NAMING,
23
+ exact: EXACT_STORAGE_NAMING,
24
+ field: EXACT_STORAGE_NAMING,
25
+ verbatim: EXACT_STORAGE_NAMING
26
+ })
27
+
28
+ const toSnakeCase = (value) => String(value)
29
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
30
+ .replace(/([A-Z]+)([A-Z][a-z0-9])/g, '$1_$2')
31
+ .toLowerCase()
32
+
33
+ export const normalizeStorageConfig = (storage = undefined) => {
34
+ if (storage === undefined || storage === null) {
35
+ return {
36
+ naming: SNAKE_CASE_STORAGE_NAMING
37
+ }
38
+ }
39
+
40
+ if (!isPlainObject(storage)) {
41
+ throw new Error('Resource storage metadata must be an object.')
42
+ }
43
+
44
+ const rawNaming = storage.naming
45
+ const naming = rawNaming === undefined
46
+ ? SNAKE_CASE_STORAGE_NAMING
47
+ : STORAGE_NAMING_ALIASES[rawNaming]
48
+
49
+ if (!naming) {
50
+ throw new Error(
51
+ `Invalid resource storage.naming value '${String(rawNaming)}'. Expected 'snake_case' or 'exact'.`
52
+ )
53
+ }
54
+
55
+ return {
56
+ ...storage,
57
+ naming
58
+ }
59
+ }
60
+
61
+ const resolveDefaultStorageColumn = (fieldName, { idColumn = 'id', storageConfig = undefined } = {}) => {
62
+ if (fieldName === 'id') {
63
+ return idColumn
64
+ }
65
+
66
+ const normalizedStorage = normalizeStorageConfig(storageConfig)
67
+ if (normalizedStorage.naming === EXACT_STORAGE_NAMING) {
68
+ return fieldName
69
+ }
70
+
71
+ return toSnakeCase(fieldName)
72
+ }
73
+
74
+ export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = 'id', storageConfig = undefined } = {}) => {
75
+ const fieldStorage = definition.storage
76
+
77
+ if (fieldStorage !== undefined && !isPlainObject(fieldStorage)) {
17
78
  throw new Error(`Field '${fieldName}' has invalid storage metadata. Expected an object.`)
18
79
  }
19
80
 
20
- const column = storage?.column ?? fieldName
81
+ const column = fieldStorage?.column ?? resolveDefaultStorageColumn(fieldName, {
82
+ idColumn,
83
+ storageConfig
84
+ })
21
85
  if (typeof column !== 'string' || column.trim() === '') {
22
86
  throw new Error(`Field '${fieldName}' has an invalid storage.column value.`)
23
87
  }
@@ -28,25 +92,29 @@ export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = '
28
92
  )
29
93
  }
30
94
 
31
- if (storage?.serialize !== undefined && typeof storage.serialize !== 'function') {
95
+ if (fieldStorage?.serialize !== undefined && typeof fieldStorage.serialize !== 'function') {
32
96
  throw new Error(`Field '${fieldName}' has an invalid storage.serialize value. Expected a function.`)
33
97
  }
34
98
 
35
99
  return {
36
100
  column,
37
- serialize: storage?.serialize || null,
101
+ serialize: fieldStorage?.serialize || null,
38
102
  persisted: definition.virtual !== true && definition.computed !== true
39
103
  }
40
104
  }
41
105
 
42
- export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id' } = {}) => {
106
+ export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id', storage = undefined } = {}) => {
43
107
  const idColumn = idProperty || 'id'
108
+ const storageConfig = normalizeStorageConfig(storage)
44
109
  const fields = {}
45
110
  const columns = {}
46
111
  const usedColumns = new Map([[idColumn, 'id']])
47
112
 
48
113
  for (const [fieldName, definition] of Object.entries(schemaStructure)) {
49
- const normalized = normalizeFieldStorage(fieldName, definition, { idColumn })
114
+ const normalized = normalizeFieldStorage(fieldName, definition, {
115
+ idColumn,
116
+ storageConfig
117
+ })
50
118
  const isIdSurrogateField = fieldName === idColumn && definition.type === 'id'
51
119
 
52
120
  if (fieldName !== 'id' && normalized.column === idColumn && !isIdSurrogateField) {
@@ -75,6 +143,7 @@ export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id' } = {
75
143
 
76
144
  return {
77
145
  idColumn,
146
+ storage: storageConfig,
78
147
  fields,
79
148
  columns
80
149
  }
@@ -91,7 +160,8 @@ export const getStorageInfo = (schemaInfo = {}) => {
91
160
 
92
161
  return buildStorageInfo({
93
162
  schemaStructure: schemaInfo.schemaStructure,
94
- idProperty: schemaInfo.idProperty || 'id'
163
+ idProperty: schemaInfo.idProperty || 'id',
164
+ storage: schemaInfo.storage
95
165
  })
96
166
  }
97
167
 
@@ -165,13 +235,13 @@ export const translateAttributesForStorage = (attributes, schemaInfo = {}, optio
165
235
  : fieldStorage?.column || fieldName
166
236
  const serializedValue = fieldStorage?.serialize
167
237
  ? fieldStorage.serialize(value, {
168
- fieldName,
169
- columnName,
170
- definition: fieldStorage.definition,
171
- schemaInfo,
172
- context,
173
- operation
174
- })
238
+ fieldName,
239
+ columnName,
240
+ definition: fieldStorage.definition,
241
+ schemaInfo,
242
+ context,
243
+ operation
244
+ })
175
245
  : value
176
246
 
177
247
  translated[columnName] = serializedValue
@@ -1,5 +1,5 @@
1
1
  import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
2
- import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-helpers.js'
2
+ import { normalizeRelationshipIdentifiers } from '../querying-writing/resource-id-normalization.js'
3
3
 
4
4
  /**
5
5
  * Updates many-to-many relationships intelligently by synchronizing pivot table records
@@ -103,7 +103,8 @@ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-hel
103
103
  export const updateManyToManyRelationship = async (scope, deps) => {
104
104
  // Extract values from deps
105
105
  const { api, context } = deps
106
- const { resourceId, relDef, relData, transaction: trx } = context
106
+ const { resourceId, relDef, transaction: trx } = context
107
+ const relData = normalizeRelationshipIdentifiers(context.relData || [], { api })
107
108
 
108
109
  // Get the knex instance from the pivot scope
109
110
  const pivotScope = api.resources[relDef.through]
@@ -135,7 +136,7 @@ export const updateManyToManyRelationship = async (scope, deps) => {
135
136
  await api.resources[related.type].get({
136
137
  id: related.id,
137
138
  transaction: trx
138
- })
139
+ }, { ...context })
139
140
  } catch (error) {
140
141
  throw new RestApiResourceError(
141
142
  `Related ${related.type} with id ${related.id} not found`,
@@ -183,6 +184,7 @@ export const updateManyToManyRelationship = async (scope, deps) => {
183
184
  * @param {Object} relDef - The relationship definition
184
185
  * @param {Array} relData - Array of related resources to link
185
186
  * @param {Object} trx - Database transaction object
187
+ * @param {Object} [context] - Caller context forwarded to relationship validation
186
188
  * @returns {Promise<void>}
187
189
  *
188
190
  * @example
@@ -256,7 +258,8 @@ export const updateManyToManyRelationship = async (scope, deps) => {
256
258
  * 5. Performs single INSERT with all records
257
259
  * 6. Returns (no data returned, throws on error)
258
260
  */
259
- export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
261
+ export const createPivotRecords = async (api, resourceId, relDef, relData, trx, context = {}) => {
262
+ relData = normalizeRelationshipIdentifiers(relData, { api })
260
263
  if (relData.length === 0) return // Early exit if nothing to create
261
264
 
262
265
  // Get pivot table info
@@ -275,7 +278,7 @@ export const createPivotRecords = async (api, resourceId, relDef, relData, trx)
275
278
  await api.resources[related.type].get({
276
279
  id: related.id,
277
280
  transaction: trx
278
- })
281
+ }, { ...context })
279
282
  } catch (error) {
280
283
  throw new RestApiResourceError(
281
284
  `Related ${related.type} with id ${related.id} not found`,
@@ -1,4 +1,5 @@
1
1
  import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
2
+ import { validateRelationshipDataCardinality } from '../querying-writing/relationship-contracts.js'
2
3
 
3
4
  /**
4
5
  * Processes JSON:API relationship data and converts to database operations
@@ -183,6 +184,14 @@ export const processRelationships = (scope, deps) => {
183
184
  fieldDef.as === relName
184
185
  )
185
186
 
187
+ if (schemaField || relDef) {
188
+ validateRelationshipDataCardinality({
189
+ relationshipName: relName,
190
+ relDef: schemaField ? schemaField[1] : relDef,
191
+ data: relData?.data
192
+ })
193
+ }
194
+
186
195
  if (schemaField) {
187
196
  const [fieldName, fieldDef] = schemaField
188
197
 
@@ -235,7 +244,7 @@ export const processRelationships = (scope, deps) => {
235
244
  foreignKey: relDef.foreignKey,
236
245
  otherKey: relDef.otherKey
237
246
  },
238
- relData: relData.data || [] // null means empty array for many-to-many
247
+ relData: relData.data
239
248
  })
240
249
  }
241
250
  // Note: hasOne and hasMany don't need processing here as they don't update the current record