json-rest-api 1.0.29 → 2.1.0

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 (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -1,17 +1,42 @@
1
- const STRING_SLOT_COUNT = 10
2
- const NUMBER_SLOT_COUNT = 10
3
- const BOOLEAN_SLOT_COUNT = 5
4
- const DATE_SLOT_COUNT = 5
5
- const JSON_SLOT_COUNT = 5
6
- const BELONGS_TO_SLOT_COUNT = 5
7
-
8
- export const SLOT_LIMITS = {
9
- string: STRING_SLOT_COUNT,
10
- number: NUMBER_SLOT_COUNT,
11
- boolean: BOOLEAN_SLOT_COUNT,
12
- date: DATE_SLOT_COUNT,
13
- json: JSON_SLOT_COUNT,
14
- belongsTo: BELONGS_TO_SLOT_COUNT,
1
+ import { hasKnexTableIndex } from '../dbIntrospection.js'
2
+
3
+ // Default layout of a new any_records table. The registry reads the actual
4
+ // columns, so a migration that adds slots raises the limits for every server.
5
+ export const SLOT_LIMITS = Object.freeze({
6
+ string: 10,
7
+ number: 10,
8
+ boolean: 5,
9
+ date: 5,
10
+ json: 5,
11
+ belongsTo: 5,
12
+ })
13
+
14
+ const SLOT_COLUMNS = {
15
+ string: index => [`string_${index}`],
16
+ number: index => [`number_${index}`],
17
+ boolean: index => [`boolean_${index}`],
18
+ date: index => [`date_${index}`],
19
+ json: index => [`json_${index}`],
20
+ belongsTo: index => [`rel_${index}_id`, `rel_${index}_type`],
21
+ }
22
+
23
+ const addSlotColumn = (table, column) => {
24
+ if (column.startsWith('number_')) table.double(column)
25
+ else if (column.startsWith('boolean_')) table.boolean(column)
26
+ else if (column.startsWith('date_')) table.dateTime(column, { precision: 6 })
27
+ else if (column.startsWith('rel_')) table.string(column)
28
+ else table.text(column)
29
+ }
30
+
31
+ const resolveSlotCounts = (slots = {}) => {
32
+ if (!slots || typeof slots !== 'object' || Array.isArray(slots)) {
33
+ throw new Error('AnyAPI slots must be an object of slot counts')
34
+ }
35
+ for (const [pool, count] of Object.entries(slots)) {
36
+ if (!Object.hasOwn(SLOT_LIMITS, pool)) throw new Error(`Unknown AnyAPI slot pool '${pool}'`)
37
+ if (!Number.isInteger(count) || count < 1) throw new Error(`AnyAPI ${pool} slot count must be a positive integer`)
38
+ }
39
+ return { ...SLOT_LIMITS, ...slots }
15
40
  }
16
41
 
17
42
  export const DEFAULT_CANONICAL_CONFIG = {
@@ -48,46 +73,31 @@ const ensureColumn = async (knex, tableName, columnName, columnBuilder) => {
48
73
  const ensureUniqueIndex = async (knex, tableName, columns, indexName) => {
49
74
  const exists = await knex.schema.hasTable(tableName)
50
75
  if (!exists) return
51
- const hasIndex = await knex.schema.withSchema('main').hasIndex?.(tableName, indexName)
76
+ const hasIndex = await hasKnexTableIndex(knex, tableName, indexName)
52
77
  if (!hasIndex) {
53
- try {
54
- await knex.schema.table(tableName, (table) => {
55
- table.unique(columns, indexName)
56
- })
57
- } catch (error) {
58
- if (!String(error?.message || '').includes('already exists')) {
59
- throw error
60
- }
61
- }
78
+ await knex.schema.table(tableName, (table) => {
79
+ table.unique(columns, indexName)
80
+ })
62
81
  }
63
82
  }
64
83
 
65
- export const ensureAnyApiSchema = async (knex) => {
84
+ /**
85
+ * Create or upgrade the shared canonical tables. Run it from a migration, or let
86
+ * the plugin run it at start-up. `slots` only adds missing slot columns; lowering
87
+ * a count never drops columns that may hold data.
88
+ * @param {import('knex').Knex} knex
89
+ * @param {{ slots?: Partial<Record<keyof typeof SLOT_LIMITS, number>> }} [options]
90
+ */
91
+ export const ensureAnyApiSchema = async (knex, { slots } = {}) => {
92
+ const counts = resolveSlotCounts(slots)
66
93
  await ensureTable(knex, 'any_records', (table) => {
67
94
  table.increments('id').primary()
68
95
  table.string('logical_id')
69
96
  table.string('tenant_id').notNullable()
70
97
  table.string('resource').notNullable()
71
98
 
72
- for (let index = 1; index <= STRING_SLOT_COUNT; index += 1) {
73
- table.text(`string_${index}`)
74
- }
75
- for (let index = 1; index <= NUMBER_SLOT_COUNT; index += 1) {
76
- table.double(`number_${index}`)
77
- }
78
- for (let index = 1; index <= BOOLEAN_SLOT_COUNT; index += 1) {
79
- table.boolean(`boolean_${index}`)
80
- }
81
- for (let index = 1; index <= DATE_SLOT_COUNT; index += 1) {
82
- table.dateTime(`date_${index}`)
83
- }
84
- for (let index = 1; index <= JSON_SLOT_COUNT; index += 1) {
85
- table.text(`json_${index}`)
86
- }
87
-
88
- for (let index = 1; index <= BELONGS_TO_SLOT_COUNT; index += 1) {
89
- table.string(`rel_${index}_id`)
90
- table.string(`rel_${index}_type`)
99
+ for (const [pool, count] of Object.entries(counts)) {
100
+ for (let index = 1; index <= count; index += 1) SLOT_COLUMNS[pool](index).forEach(column => addSlotColumn(table, column))
91
101
  }
92
102
 
93
103
  table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable()
@@ -99,6 +109,28 @@ export const ensureAnyApiSchema = async (knex) => {
99
109
  table.index(['resource'])
100
110
  })
101
111
 
112
+ if (slots) {
113
+ const existing = new Set(Object.keys(await knex('any_records').columnInfo()))
114
+ const missing = Object.entries(counts).flatMap(([pool, count]) => Array.from({ length: count }, (_, index) => SLOT_COLUMNS[pool](index + 1)))
115
+ .flat()
116
+ .filter(column => !existing.has(column))
117
+ if (missing.length) {
118
+ await knex.schema.alterTable('any_records', (table) => missing.forEach(column => addSlotColumn(table, column)))
119
+ }
120
+ }
121
+
122
+ const client = knex.client.config.client
123
+ if (['pg', 'postgresql', 'mysql', 'mysql2'].includes(client)) {
124
+ const columns = (await knex('information_schema.columns')
125
+ .select({ column: 'column_name', precision: 'datetime_precision' })
126
+ .where('table_schema', knex.raw(client === 'pg' || client === 'postgresql' ? 'current_schema()' : 'DATABASE()'))
127
+ .where('table_name', 'any_records'))
128
+ .filter(column => /^date_\d+$/.test(column.column))
129
+ if (!columns.length || columns.some(column => Number(column.precision) !== 6)) {
130
+ throw new Error('AnyAPI temporal storage migration required: every any_records date slot must use timestamp/datetime precision 6. See 33-migrating-to-v2.md.')
131
+ }
132
+ }
133
+
102
134
  await ensureColumn(knex, 'any_records', 'logical_id', (table) => {
103
135
  table.string('logical_id')
104
136
  })
@@ -146,15 +178,24 @@ export const ensureAnyApiSchema = async (knex) => {
146
178
  table.string('resource').notNullable()
147
179
  table.text('schema_json').notNullable()
148
180
  table.text('relationships_json').notNullable()
181
+ // Holds the search schema and runtime additions too: MySQL TEXT stops at 64 KB.
182
+ table.text('definition_json', 'longtext')
183
+ table.integer('schema_version').notNullable().defaultTo(1)
149
184
  table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable()
150
185
  table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable()
151
186
 
152
187
  table.unique(['tenant_id', 'resource'])
153
188
  })
189
+ await ensureColumn(knex, 'any_resource_configs', 'definition_json', (table) => {
190
+ table.text('definition_json', 'longtext')
191
+ })
192
+ await ensureColumn(knex, 'any_resource_configs', 'schema_version', (table) => {
193
+ table.integer('schema_version').notNullable().defaultTo(1)
194
+ })
154
195
 
155
196
  await ensureTable(knex, 'any_field_configs', (table) => {
156
197
  table.increments('id').primary()
157
- table.integer('resource_config_id').notNullable()
198
+ table.integer('resource_config_id').unsigned().notNullable()
158
199
  .references('id').inTable('any_resource_configs').onDelete('CASCADE')
159
200
  table.string('field_name').notNullable()
160
201
  table.string('slot_type').notNullable()
@@ -169,12 +210,24 @@ export const ensureAnyApiSchema = async (knex) => {
169
210
  table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable()
170
211
 
171
212
  table.unique(['resource_config_id', 'field_name'])
213
+ table.unique(['resource_config_id', 'slot_column'], 'any_field_configs_resource_slot_unique')
172
214
  table.index(['resource_config_id'])
173
215
  })
216
+ if (!await hasKnexTableIndex(knex, 'any_field_configs', 'any_field_configs_resource_slot_unique')) {
217
+ const shared = await knex('any_field_configs')
218
+ .select('resource_config_id', 'slot_column')
219
+ .groupBy('resource_config_id', 'slot_column')
220
+ .havingRaw('count(*) > 1')
221
+ .first()
222
+ if (shared) {
223
+ throw new Error(`AnyAPI slot metadata repair required: several fields of resource config ${shared.resource_config_id} share ${shared.slot_column}. Reassign them before upgrading.`)
224
+ }
225
+ await ensureUniqueIndex(knex, 'any_field_configs', ['resource_config_id', 'slot_column'], 'any_field_configs_resource_slot_unique')
226
+ }
174
227
 
175
228
  await ensureTable(knex, 'any_relationship_configs', (table) => {
176
229
  table.increments('id').primary()
177
- table.integer('resource_config_id').notNullable()
230
+ table.integer('resource_config_id').unsigned().notNullable()
178
231
  .references('id').inTable('any_resource_configs').onDelete('CASCADE')
179
232
  table.string('relationship_name').notNullable()
180
233
  table.string('relationship_type').notNullable()
@@ -191,18 +244,28 @@ export const ensureAnyApiSchema = async (knex) => {
191
244
  table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable()
192
245
  table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable()
193
246
 
194
- table.unique(['resource_config_id', 'relationship_name'])
247
+ table.unique(['resource_config_id', 'relationship_name'], 'any_relationship_configs_resource_relationship_unique')
195
248
  table.index(['resource_config_id'])
196
249
  })
197
250
  }
198
251
 
199
- export const SLOT_POOLS = {
200
- string: Array.from({ length: STRING_SLOT_COUNT }, (_, index) => `string_${index + 1}`),
201
- number: Array.from({ length: NUMBER_SLOT_COUNT }, (_, index) => `number_${index + 1}`),
202
- boolean: Array.from({ length: BOOLEAN_SLOT_COUNT }, (_, index) => `boolean_${index + 1}`),
203
- date: Array.from({ length: DATE_SLOT_COUNT }, (_, index) => `date_${index + 1}`),
204
- json: Array.from({ length: JSON_SLOT_COUNT }, (_, index) => `json_${index + 1}`),
205
- belongsTo: Array.from({ length: BELONGS_TO_SLOT_COUNT }, (_, index) => index + 1),
252
+ const buildSlotPools = (hasColumns) => Object.fromEntries(Object.keys(SLOT_LIMITS).map(pool => {
253
+ const indexes = []
254
+ for (let index = 1; hasColumns(pool, index); index += 1) indexes.push(index)
255
+ return [pool, pool === 'belongsTo' ? indexes : indexes.map(index => SLOT_COLUMNS[pool](index)[0])]
256
+ }))
257
+
258
+ export const SLOT_POOLS = buildSlotPools((pool, index) => index <= SLOT_LIMITS[pool])
259
+
260
+ /**
261
+ * Read the slot columns any_records actually has. Slots are numbered from 1
262
+ * without gaps; a pool ends at its first missing column.
263
+ * @param {import('knex').Knex | import('knex').Knex.Transaction} db
264
+ */
265
+ export const readSlotPools = async (db) => {
266
+ const columns = new Set(Object.keys(await db('any_records').columnInfo()))
267
+ if (!columns.size) throw new Error('AnyAPI storage tables are missing; run ensureAnyApiSchema in a migration')
268
+ return buildSlotPools((pool, index) => SLOT_COLUMNS[pool](index).every(column => columns.has(column)))
206
269
  }
207
270
 
208
271
  export const TYPE_TO_POOL = new Map([
@@ -216,12 +279,13 @@ export const TYPE_TO_POOL = new Map([
216
279
  ['float', 'number'],
217
280
  ['decimal', 'number'],
218
281
  ['boolean', 'boolean'],
219
- ['date', 'date'],
282
+ ['date', 'string'],
220
283
  ['datetime', 'date'],
221
284
  ['dateTime', 'date'],
222
- ['time', 'date'],
285
+ ['time', 'string'],
223
286
  ['epochMilliseconds', 'number'],
224
287
  ['epochSeconds', 'number'],
225
288
  ['json', 'json'],
226
289
  ['object', 'json'],
290
+ ['array', 'json'],
227
291
  ])
@@ -0,0 +1,109 @@
1
+ // Stored resource definitions keep JSON values only. The paths of values JSON
2
+ // cannot hold (callbacks, class instances, RegExps) are recorded, so a server
3
+ // loading the definition knows which fields need code it may not have.
4
+
5
+ const isPlainObject = value => {
6
+ const prototype = Object.getPrototypeOf(value)
7
+ return prototype === Object.prototype || prototype === null
8
+ }
9
+
10
+ /**
11
+ * @param {unknown} value
12
+ * @returns {{ json: any, callbacks: Array<Array<string|number>> }}
13
+ */
14
+ export const toStoredJson = (value) => {
15
+ const callbacks = []
16
+ const visit = (current, path) => {
17
+ if (current === undefined) return undefined
18
+ if (current === null || typeof current === 'string' || typeof current === 'boolean') return current
19
+ if (typeof current === 'number' && Number.isFinite(current)) return current
20
+ if (Array.isArray(current)) return current.map((item, index) => visit(item, [...path, index]) ?? null)
21
+ if (typeof current === 'object' && isPlainObject(current)) {
22
+ const result = {}
23
+ for (const [key, item] of Object.entries(current)) {
24
+ const stored = visit(item, [...path, key])
25
+ if (stored !== undefined) result[key] = stored
26
+ }
27
+ return result
28
+ }
29
+ callbacks.push(path)
30
+ return undefined
31
+ }
32
+ return { json: visit(value, []), callbacks }
33
+ }
34
+
35
+ const targetsOf = definition => definition?.oneOf || [definition?.actualField].filter(Boolean)
36
+
37
+ /**
38
+ * Combine a resource's declaration with the fields other servers added at run
39
+ * time. Declared fields win; runtime fields this server added keep its local
40
+ * callbacks. A field whose callbacks exist in no available code is hidden
41
+ * rather than served without them, because a missing serializer or setter
42
+ * would read or write the wrong values.
43
+ * @param {{ declaration?: Record<string, any>, stored?: Record<string, any> | null, local?: { schema?: Record<string, any>, searchSchema?: Record<string, any> }, fromStorage?: boolean }} input
44
+ */
45
+ export const composeResourceOptions = ({ declaration = {}, stored = null, local = {}, fromStorage = false }) => {
46
+ const hidden = new Set()
47
+ const hiddenFilters = new Set()
48
+ const declaredSchema = declaration.schema || {}
49
+ const declaredSearch = declaration.searchSchema || {}
50
+ const schema = { ...declaredSchema }
51
+ const searchSchema = { ...declaredSearch }
52
+
53
+ if (fromStorage) {
54
+ for (const path of stored?.callbacks || []) {
55
+ if (path[0] === 'schema' && path.length > 1) hidden.add(path[1])
56
+ else if (path[0] === 'searchSchema' && path.length > 1) hiddenFilters.add(path[1])
57
+ else throw new Error(`Resource option '${path.join('.')}' cannot be loaded from storage; declare the resource in code`)
58
+ }
59
+ }
60
+
61
+ const runtime = stored?.runtime || {}
62
+ const needsCode = new Set((runtime.callbacks || []).map(path => `${path[0]}.${path[1]}`))
63
+ const merge = (kind, target, declared, localDefinitions = {}, hide) => {
64
+ for (const [name, definition] of Object.entries(runtime[kind] || {})) {
65
+ if (Object.hasOwn(declared, name)) continue
66
+ if (Object.hasOwn(localDefinitions, name)) target[name] = localDefinitions[name]
67
+ else if (needsCode.has(`${kind}.${name}`)) hide.add(name)
68
+ else target[name] = definition
69
+ }
70
+ }
71
+ merge('schema', schema, declaredSchema, local.schema, hidden)
72
+ merge('searchSchema', searchSchema, declaredSearch, local.searchSchema, hiddenFilters)
73
+
74
+ for (const name of hidden) delete schema[name]
75
+ for (const [name, definition] of Object.entries(searchSchema)) {
76
+ if (hiddenFilters.has(name) || targetsOf(definition).some(target => hidden.has(target))) {
77
+ hiddenFilters.add(name)
78
+ delete searchSchema[name]
79
+ }
80
+ }
81
+
82
+ const options = { ...declaration, schema }
83
+ if (Object.keys(searchSchema).length || declaration.searchSchema) options.searchSchema = searchSchema
84
+ if (hidden.size && Array.isArray(declaration.sortableFields)) {
85
+ options.sortableFields = declaration.sortableFields.filter(field => !hidden.has(String(field).replace(/^-/, '')))
86
+ }
87
+ return { options, hidden: [...hidden], hiddenFilters: [...hiddenFilters] }
88
+ }
89
+
90
+ /**
91
+ * Resources a stored declaration refers to. Polymorphic targets must be
92
+ * registered before the resource itself; the others can follow.
93
+ * @param {Record<string, any>} options
94
+ */
95
+ export const relatedResources = (options = {}) => {
96
+ const before = new Set()
97
+ const after = new Set()
98
+ for (const definition of Object.values(options.schema || {})) {
99
+ if (definition?.belongsTo) after.add(definition.belongsTo)
100
+ }
101
+ for (const [name, definition] of Object.entries(options.relationships || {})) {
102
+ for (const type of definition?.belongsToPolymorphic?.types || []) before.add(type)
103
+ if (definition?.type === 'hasMany' || definition?.type === 'manyToMany' || definition?.hasMany) {
104
+ after.add(definition.target || definition.hasMany || name)
105
+ }
106
+ if (definition?.through) after.add(definition.through)
107
+ }
108
+ return { before: [...before], after: [...after].filter(name => !before.has(name)) }
109
+ }
@@ -1,6 +1,3 @@
1
- import { RestApiValidationError } from '../../../../../lib/rest-api-errors.js'
2
- import { normalizeValueForDatabaseStorage } from '../../querying-writing/database-value-normalizers.js'
3
-
4
1
  export const normalizeId = (value) => (value === null || value === undefined ? null : String(value))
5
2
 
6
3
  export const findSchemaFieldByAlias = (descriptor, alias) => {
@@ -61,64 +58,3 @@ export const resolveFieldInfo = (descriptor, field) => {
61
58
 
62
59
  return null
63
60
  }
64
-
65
- export const coerceValueForDefinition = (value, definition, { isRelationship } = {}) => {
66
- if (value === null || value === undefined) return null
67
-
68
- if (isRelationship) {
69
- return normalizeId(value)
70
- }
71
-
72
- const type = definition?.type || definition?.dataType
73
- if (!type) {
74
- return value
75
- }
76
-
77
- if (['number', 'integer', 'float', 'decimal'].includes(type)) {
78
- const numeric = Number(value)
79
- return Number.isNaN(numeric) ? value : numeric
80
- }
81
-
82
- if (type === 'boolean') {
83
- if (typeof value === 'boolean') return value
84
- if (typeof value === 'string') {
85
- if (value.toLowerCase() === 'true') return true
86
- if (value.toLowerCase() === 'false') return false
87
- }
88
- return Boolean(value)
89
- }
90
-
91
- if (['string', 'text', 'uuid', 'email'].includes(type)) {
92
- return String(value)
93
- }
94
-
95
- if (['date', 'dateTime', 'time'].includes(type)) {
96
- return normalizeValueForDatabaseStorage(value, type, {
97
- temporalPrecision: definition?.temporalPrecision
98
- })
99
- }
100
-
101
- return value
102
- }
103
-
104
- export const normalizeFilterValues = (rawValue, definition, options) => {
105
- if (Array.isArray(rawValue)) {
106
- return rawValue.map((item) => coerceValueForDefinition(item, definition, options))
107
- }
108
- return [coerceValueForDefinition(rawValue, definition, options)]
109
- }
110
-
111
- export const ensureFilterableField = (descriptor, field) => {
112
- const fieldInfo = resolveFieldInfo(descriptor, field)
113
- if (!fieldInfo?.column) {
114
- throw new RestApiValidationError('Invalid filter field', {
115
- fields: [`filters.${field}`],
116
- violations: [{
117
- field: `filters.${field}`,
118
- rule: 'unknown_field',
119
- message: `Filter on '${field}' is not supported in AnyAPI mode`,
120
- }],
121
- })
122
- }
123
- return fieldInfo
124
- }