json-rest-api 1.0.10 → 1.0.13
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.
- package/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +7 -11
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- 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
|
+
}
|