json-rest-api 1.0.10 → 1.0.11

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 (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +5 -9
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -0,0 +1,186 @@
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,
15
+ };
16
+
17
+ export const DEFAULT_CANONICAL_CONFIG = {
18
+ scopeName: 'anyapi_records',
19
+ tableName: 'any_records',
20
+ type: 'anyapi_records',
21
+ tenantColumn: 'tenant_id',
22
+ resourceColumn: 'resource',
23
+ relationshipColumn: 'relationship',
24
+ leftIdColumn: 'left_id',
25
+ leftResourceColumn: 'left_resource',
26
+ rightIdColumn: 'right_id',
27
+ rightResourceColumn: 'right_resource',
28
+ };
29
+
30
+ const ensureTable = async (knex, tableName, tableBuilder) => {
31
+ const exists = await knex.schema.hasTable(tableName);
32
+ if (!exists) {
33
+ await knex.schema.createTable(tableName, tableBuilder);
34
+ }
35
+ };
36
+
37
+ const ensureIndex = async (knex, tableName, columns, indexName) => {
38
+ const exists = await knex.schema.hasTable(tableName);
39
+ if (!exists) return;
40
+ const hasIndex = await knex.schema.withSchema('main').hasIndex?.(tableName, indexName);
41
+ if (!hasIndex) {
42
+ await knex.schema.table(tableName, (table) => {
43
+ table.index(columns, indexName);
44
+ });
45
+ }
46
+ };
47
+
48
+ export const ensureAnyApiSchema = async (knex) => {
49
+ await ensureTable(knex, 'any_records', (table) => {
50
+ table.increments('id').primary();
51
+ table.string('tenant_id').notNullable();
52
+ table.string('resource').notNullable();
53
+
54
+ for (let index = 1; index <= STRING_SLOT_COUNT; index += 1) {
55
+ table.text(`string_${index}`);
56
+ }
57
+ for (let index = 1; index <= NUMBER_SLOT_COUNT; index += 1) {
58
+ table.double(`number_${index}`);
59
+ }
60
+ for (let index = 1; index <= BOOLEAN_SLOT_COUNT; index += 1) {
61
+ table.boolean(`boolean_${index}`);
62
+ }
63
+ for (let index = 1; index <= DATE_SLOT_COUNT; index += 1) {
64
+ table.dateTime(`date_${index}`);
65
+ }
66
+ for (let index = 1; index <= JSON_SLOT_COUNT; index += 1) {
67
+ table.text(`json_${index}`);
68
+ }
69
+
70
+ for (let index = 1; index <= BELONGS_TO_SLOT_COUNT; index += 1) {
71
+ table.string(`rel_${index}_id`);
72
+ table.string(`rel_${index}_type`);
73
+ }
74
+
75
+ table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
76
+ table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable();
77
+ table.dateTime('deleted_at');
78
+
79
+ table.index(['tenant_id', 'resource']);
80
+ table.index(['resource']);
81
+ });
82
+
83
+ await ensureTable(knex, 'any_links', (table) => {
84
+ table.increments('id').primary();
85
+ table.string('tenant_id').notNullable();
86
+ table.string('relationship').notNullable();
87
+ table.string('inverse_relationship');
88
+ table.string('left_resource').notNullable();
89
+ table.string('left_id').notNullable();
90
+ table.string('right_resource').notNullable();
91
+ table.string('right_id').notNullable();
92
+ table.text('payload');
93
+ table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
94
+ table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable();
95
+
96
+ table.index(['tenant_id', 'relationship']);
97
+ table.index(['tenant_id', 'inverse_relationship']);
98
+ table.index(['tenant_id', 'left_resource']);
99
+ table.index(['tenant_id', 'right_resource']);
100
+ table.index(['relationship']);
101
+ table.index(['inverse_relationship']);
102
+ });
103
+
104
+ await ensureTable(knex, 'any_resource_configs', (table) => {
105
+ table.increments('id').primary();
106
+ table.string('tenant_id').notNullable();
107
+ table.string('resource').notNullable();
108
+ table.text('schema_json').notNullable();
109
+ table.text('relationships_json').notNullable();
110
+ table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
111
+ table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable();
112
+
113
+ table.unique(['tenant_id', 'resource']);
114
+ });
115
+
116
+ await ensureTable(knex, 'any_field_configs', (table) => {
117
+ table.increments('id').primary();
118
+ table.integer('resource_config_id').notNullable()
119
+ .references('id').inTable('any_resource_configs').onDelete('CASCADE');
120
+ table.string('field_name').notNullable();
121
+ table.string('slot_type').notNullable();
122
+ table.integer('slot_index').notNullable();
123
+ table.string('slot_column').notNullable();
124
+ table.boolean('nullable').defaultTo(false).notNullable();
125
+ table.boolean('required').defaultTo(false).notNullable();
126
+ table.string('target_resource');
127
+ table.string('alias');
128
+ table.text('meta_json');
129
+ table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
130
+ table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable();
131
+
132
+ table.unique(['resource_config_id', 'field_name']);
133
+ table.index(['resource_config_id']);
134
+ });
135
+
136
+ await ensureTable(knex, 'any_relationship_configs', (table) => {
137
+ table.increments('id').primary();
138
+ table.integer('resource_config_id').notNullable()
139
+ .references('id').inTable('any_resource_configs').onDelete('CASCADE');
140
+ table.string('relationship_name').notNullable();
141
+ table.string('relationship_type').notNullable();
142
+ table.string('target_resource');
143
+ table.integer('slot_index');
144
+ table.string('id_column');
145
+ table.string('type_column');
146
+ table.string('relationship_key');
147
+ table.string('through');
148
+ table.string('foreign_key');
149
+ table.string('other_key');
150
+ table.string('alias');
151
+ table.text('meta_json');
152
+ table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
153
+ table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable();
154
+
155
+ table.unique(['resource_config_id', 'relationship_name']);
156
+ table.index(['resource_config_id']);
157
+ });
158
+ };
159
+
160
+ export const SLOT_POOLS = {
161
+ string: Array.from({ length: STRING_SLOT_COUNT }, (_, index) => `string_${index + 1}`),
162
+ number: Array.from({ length: NUMBER_SLOT_COUNT }, (_, index) => `number_${index + 1}`),
163
+ boolean: Array.from({ length: BOOLEAN_SLOT_COUNT }, (_, index) => `boolean_${index + 1}`),
164
+ date: Array.from({ length: DATE_SLOT_COUNT }, (_, index) => `date_${index + 1}`),
165
+ json: Array.from({ length: JSON_SLOT_COUNT }, (_, index) => `json_${index + 1}`),
166
+ belongsTo: Array.from({ length: BELONGS_TO_SLOT_COUNT }, (_, index) => index + 1),
167
+ };
168
+
169
+ export const TYPE_TO_POOL = new Map([
170
+ ['string', 'string'],
171
+ ['text', 'string'],
172
+ ['uuid', 'string'],
173
+ ['email', 'string'],
174
+ ['number', 'number'],
175
+ ['integer', 'number'],
176
+ ['float', 'number'],
177
+ ['decimal', 'number'],
178
+ ['boolean', 'boolean'],
179
+ ['date', 'date'],
180
+ ['datetime', 'date'],
181
+ ['dateTime', 'date'],
182
+ ['timestamp', 'date'],
183
+ ['time', 'date'],
184
+ ['json', 'json'],
185
+ ['object', 'json'],
186
+ ]);
@@ -0,0 +1,113 @@
1
+ import { RestApiValidationError } from '../../../../../lib/rest-api-errors.js';
2
+
3
+ export const normalizeId = (value) => (value === null || value === undefined ? null : String(value));
4
+
5
+ export const findSchemaFieldByAlias = (descriptor, alias) => {
6
+ if (!descriptor?.schema) return null;
7
+ for (const [fieldName, definition] of Object.entries(descriptor.schema)) {
8
+ if (!definition) continue;
9
+ if (definition.as === alias) {
10
+ return { fieldName, definition };
11
+ }
12
+ if (definition.belongsTo && !definition.as) {
13
+ const inferredAlias = fieldName.endsWith('_id') ? fieldName.slice(0, -3) : fieldName;
14
+ if (inferredAlias === alias) {
15
+ return { fieldName, definition };
16
+ }
17
+ }
18
+ }
19
+ return null;
20
+ };
21
+
22
+ export const resolveFieldInfo = (descriptor, field) => {
23
+ if (!descriptor) return null;
24
+ if (field === 'id') {
25
+ return { column: 'id', definition: { type: 'id' } };
26
+ }
27
+
28
+ const directField = descriptor.fields?.[field];
29
+ if (directField?.slot) {
30
+ return {
31
+ column: directField.slot,
32
+ definition: descriptor.schema?.[field] || null,
33
+ };
34
+ }
35
+
36
+ const belongsToInfo = descriptor.belongsTo?.[field];
37
+ if (belongsToInfo?.idColumn) {
38
+ const schemaField = findSchemaFieldByAlias(descriptor, field);
39
+ return {
40
+ column: belongsToInfo.idColumn,
41
+ definition: schemaField?.definition || null,
42
+ isRelationship: true,
43
+ };
44
+ }
45
+
46
+ const aliasField = findSchemaFieldByAlias(descriptor, field);
47
+ if (aliasField) {
48
+ const fieldEntry = descriptor.fields?.[aliasField.fieldName];
49
+ if (fieldEntry?.slot) {
50
+ return {
51
+ column: fieldEntry.slot,
52
+ definition: aliasField.definition,
53
+ };
54
+ }
55
+ }
56
+
57
+ return null;
58
+ };
59
+
60
+ export const coerceValueForDefinition = (value, definition, { isRelationship } = {}) => {
61
+ if (value === null || value === undefined) return null;
62
+
63
+ if (isRelationship) {
64
+ return normalizeId(value);
65
+ }
66
+
67
+ const type = definition?.type || definition?.dataType;
68
+ if (!type) {
69
+ return value;
70
+ }
71
+
72
+ if (['number', 'integer', 'float', 'decimal'].includes(type)) {
73
+ const numeric = Number(value);
74
+ return Number.isNaN(numeric) ? value : numeric;
75
+ }
76
+
77
+ if (type === 'boolean') {
78
+ if (typeof value === 'boolean') return value;
79
+ if (typeof value === 'string') {
80
+ if (value.toLowerCase() === 'true') return true;
81
+ if (value.toLowerCase() === 'false') return false;
82
+ }
83
+ return Boolean(value);
84
+ }
85
+
86
+ if (['string', 'text', 'uuid', 'email'].includes(type)) {
87
+ return String(value);
88
+ }
89
+
90
+ return value;
91
+ };
92
+
93
+ export const normalizeFilterValues = (rawValue, definition, options) => {
94
+ if (Array.isArray(rawValue)) {
95
+ return rawValue.map((item) => coerceValueForDefinition(item, definition, options));
96
+ }
97
+ return [coerceValueForDefinition(rawValue, definition, options)];
98
+ };
99
+
100
+ export const ensureFilterableField = (descriptor, field) => {
101
+ const fieldInfo = resolveFieldInfo(descriptor, field);
102
+ if (!fieldInfo?.column) {
103
+ throw new RestApiValidationError('Invalid filter field', {
104
+ fields: [`filters.${field}`],
105
+ violations: [{
106
+ field: `filters.${field}`,
107
+ rule: 'unknown_field',
108
+ message: `Filter on '${field}' is not supported in AnyAPI mode`,
109
+ }],
110
+ });
111
+ }
112
+ return fieldInfo;
113
+ };
@@ -0,0 +1,305 @@
1
+ /**
2
+ * @file Creates Knex table definitions from json-rest-schema definitions
3
+ */
4
+
5
+ /**
6
+ * Maps json-rest-schema types to Knex column types
7
+ * @param {object} table - The Knex table builder instance
8
+ * @param {string} columnName - The name of the column
9
+ * @param {object} definition - The schema definition for the field
10
+ * @returns {object} The Knex column builder instance
11
+ */
12
+ function mapTypeToKnex(table, columnName, definition) {
13
+ const { type, precision, scale, maxLength, unsigned } = definition;
14
+
15
+ switch (type) {
16
+ case 'string':
17
+ return maxLength ? table.string(columnName, maxLength) : table.string(columnName);
18
+
19
+ case 'number':
20
+ if (precision !== undefined && scale !== undefined) {
21
+ return table.decimal(columnName, precision, scale);
22
+ }
23
+ return table.float(columnName);
24
+
25
+ case 'id':
26
+ const col = table.integer(columnName);
27
+ return unsigned !== false ? col.unsigned() : col;
28
+
29
+ case 'boolean':
30
+ return table.boolean(columnName);
31
+
32
+ case 'date':
33
+ return table.date(columnName);
34
+
35
+ case 'dateTime':
36
+ return table.datetime(columnName);
37
+
38
+ case 'time':
39
+ return table.time(columnName);
40
+
41
+ case 'timestamp':
42
+ return table.integer(columnName);
43
+
44
+ case 'array':
45
+ case 'object':
46
+ case 'serialize':
47
+ return table.json(columnName);
48
+
49
+ case 'blob':
50
+ case 'file':
51
+ return table.binary(columnName);
52
+
53
+ case 'none':
54
+ default:
55
+ return table.string(columnName);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Applies schema constraints to a Knex column
61
+ * @param {object} column - The Knex column builder instance
62
+ * @param {object} definition - The schema definition for the field
63
+ */
64
+ function applyConstraints(column, definition) {
65
+ // Nullability
66
+ if (definition.nullable === true) {
67
+ column.nullable();
68
+ } else if (definition.required === true || definition.nullable === false) {
69
+ column.notNullable();
70
+ }
71
+
72
+ // Default value
73
+ if (definition.defaultTo !== undefined) {
74
+ column.defaultTo(definition.defaultTo);
75
+ }
76
+
77
+ // Database-specific constraints
78
+ if (definition.unique === true) {
79
+ column.unique();
80
+ }
81
+
82
+ if (definition.primary === true) {
83
+ column.primary();
84
+ }
85
+
86
+ if (definition.index === true) {
87
+ column.index();
88
+ }
89
+
90
+ if (definition.references) {
91
+ column.references(definition.references.column || 'id')
92
+ .inTable(definition.references.table);
93
+
94
+ if (definition.references.onDelete) {
95
+ column.onDelete(definition.references.onDelete);
96
+ }
97
+
98
+ if (definition.references.onUpdate) {
99
+ column.onUpdate(definition.references.onUpdate);
100
+ }
101
+ }
102
+
103
+ if (definition.comment) {
104
+ column.comment(definition.comment);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Creates a Knex table from a json-rest-schema definition
110
+ * @param {object} knex - The Knex instance
111
+ * @param {string} tableName - The name of the table to create
112
+ * @param {object} schema - The json-rest-schema instance
113
+ * @param {string} [idProperty='id'] - The name of the ID column
114
+ * @param {object} [options={}] - Additional options
115
+ * @param {boolean} [options.autoIncrement=true] - Whether to use auto-incrementing IDs
116
+ * @param {boolean} [options.timestamps=false] - Whether to add created_at/updated_at columns
117
+ * @returns {Promise} A promise that resolves when the table is created
118
+ */
119
+ export async function createKnexTable(knex, schemaInfo, tableSchemaInstance, options = {}) {
120
+ const { autoIncrement = true, timestamps = false } = options;
121
+
122
+
123
+ const tableName = schemaInfo.tableName
124
+ const idProperty = schemaInfo.idProperty
125
+ const tableSchemaStructure = tableSchemaInstance.structure
126
+
127
+ return knex.schema.createTable(tableName, (table) => {
128
+ // Check if schema has the idProperty field with primary key
129
+ const hasIdField = tableSchemaStructure[idProperty] && tableSchemaStructure[idProperty].primary === true;
130
+
131
+ // Add auto-incrementing ID if no primary key is defined and autoIncrement is true
132
+ if (!hasIdField && autoIncrement) {
133
+ table.increments(idProperty).primary();
134
+ }
135
+
136
+ // Process each field in the schema
137
+ for (const [fieldName, definition] of Object.entries(tableSchemaStructure)) {
138
+ // Skip if this is the ID field and we already handled it
139
+ if (fieldName === idProperty && !hasIdField && autoIncrement) {
140
+ continue;
141
+ }
142
+
143
+ // Create the column with the appropriate type
144
+ const column = mapTypeToKnex(table, fieldName, definition);
145
+
146
+ // Apply constraints
147
+ applyConstraints(column, definition);
148
+ }
149
+
150
+ // Add timestamps if requested
151
+ if (timestamps) {
152
+ table.timestamps(true, true);
153
+ }
154
+ });
155
+ }
156
+
157
+ export async function addKnexFields(knex, tableName, schema) {
158
+ return knex.schema.alterTable(tableName, (table) => {
159
+ for (const [fieldName, definition] of Object.entries(schema.structure)) {
160
+ const column = mapTypeToKnex(table, fieldName, definition);
161
+ applyConstraints(column, definition);
162
+ }
163
+ });
164
+ }
165
+
166
+ // Helper function to alter multiple fields in an existing table
167
+ export async function alterKnexFields(knex, tableName, fields, options = {}) {
168
+ return knex.schema.alterTable(tableName, (table) => {
169
+ for (const [fieldName, definition] of Object.entries(fields)) {
170
+ // Create the column with alter flag
171
+ const column = mapTypeToKnex(table, fieldName, definition);
172
+
173
+ // Mark this as an alteration
174
+ column.alter();
175
+
176
+ // Apply constraints (these will be reapplied with the alter)
177
+ applyConstraints(column, definition);
178
+ }
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Generates a Knex migration string from a json-rest-schema definition
184
+ * @param {string} tableName - The name of the table
185
+ * @param {object} schema - The json-rest-schema instance
186
+ * @param {object} [options={}] - Additional options
187
+ * @returns {string} The migration code as a string
188
+ */
189
+ export function generateKnexMigration(tableName, schema, options = {}) {
190
+ const { autoIncrement = true, timestamps = false } = options;
191
+
192
+ let migration = `exports.up = function(knex) {
193
+ return knex.schema.createTable('${tableName}', (table) => {\n`;
194
+
195
+ // Check if schema has an 'id' field with primary key
196
+ const hasIdField = schema.structure.id && schema.structure.id.primary === true;
197
+
198
+ // Add auto-incrementing ID if needed
199
+ if (!hasIdField && autoIncrement) {
200
+ migration += ` table.increments('id').primary();\n`;
201
+ }
202
+
203
+ // Process each field
204
+ for (const [fieldName, definition] of Object.entries(schema.structure)) {
205
+ if (fieldName === 'id' && !hasIdField && autoIncrement) {
206
+ continue;
207
+ }
208
+
209
+ let line = ' ';
210
+
211
+ // Map type
212
+ switch (definition.type) {
213
+ case 'string':
214
+ line += definition.maxLength
215
+ ? `table.string('${fieldName}', ${definition.maxLength})`
216
+ : `table.string('${fieldName}')`;
217
+ break;
218
+ case 'number':
219
+ if (definition.precision !== undefined && definition.scale !== undefined) {
220
+ line += `table.decimal('${fieldName}', ${definition.precision}, ${definition.scale})`;
221
+ } else {
222
+ line += `table.float('${fieldName}')`;
223
+ }
224
+ break;
225
+ case 'id':
226
+ line += `table.integer('${fieldName}')`;
227
+ if (definition.unsigned !== false) line += '.unsigned()';
228
+ break;
229
+ case 'boolean':
230
+ line += `table.boolean('${fieldName}')`;
231
+ break;
232
+ case 'date':
233
+ line += `table.date('${fieldName}')`;
234
+ break;
235
+ case 'dateTime':
236
+ line += `table.datetime('${fieldName}')`;
237
+ break;
238
+ case 'time':
239
+ line += `table.time('${fieldName}')`;
240
+ break;
241
+ case 'timestamp':
242
+ line += `table.integer('${fieldName}')`;
243
+ break;
244
+ case 'array':
245
+ case 'object':
246
+ case 'serialize':
247
+ line += `table.json('${fieldName}')`;
248
+ break;
249
+ case 'blob':
250
+ case 'file':
251
+ line += `table.binary('${fieldName}')`;
252
+ break;
253
+ default:
254
+ line += `table.string('${fieldName}')`;
255
+ }
256
+
257
+ // Add constraints
258
+ if (definition.nullable === true) {
259
+ line += '.nullable()';
260
+ } else if (definition.required === true || definition.nullable === false) {
261
+ line += '.notNullable()';
262
+ }
263
+
264
+ if (definition.defaultTo !== undefined) {
265
+ const defaultValue = typeof definition.defaultTo === 'string'
266
+ ? `'${definition.defaultTo}'`
267
+ : definition.defaultTo;
268
+ line += `.defaultTo(${defaultValue})`;
269
+ }
270
+
271
+ if (definition.unique === true) line += '.unique()';
272
+ if (definition.primary === true) line += '.primary()';
273
+ if (definition.index === true) line += '.index()';
274
+
275
+ if (definition.references) {
276
+ const refCol = definition.references.column || 'id';
277
+ line += `.references('${refCol}').inTable('${definition.references.table}')`;
278
+ if (definition.references.onDelete) {
279
+ line += `.onDelete('${definition.references.onDelete}')`;
280
+ }
281
+ if (definition.references.onUpdate) {
282
+ line += `.onUpdate('${definition.references.onUpdate}')`;
283
+ }
284
+ }
285
+
286
+ if (definition.comment) {
287
+ line += `.comment('${definition.comment}')`;
288
+ }
289
+
290
+ migration += line + ';\n';
291
+ }
292
+
293
+ if (timestamps) {
294
+ migration += ` table.timestamps(true, true);\n`;
295
+ }
296
+
297
+ migration += ` });
298
+ };
299
+
300
+ exports.down = function(knex) {
301
+ return knex.schema.dropTable('${tableName}');
302
+ };`;
303
+
304
+ return migration;
305
+ }