@vida-global/core 2.3.2 → 2.3.4
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/AGENTS.md +45 -4
- package/lib/activeRecord/README.md +24 -2
- package/lib/activeRecord/afterCommitHooks.js +167 -0
- package/lib/activeRecord/baseRecord.js +91 -26
- package/lib/activeRecord/db/migrator.js +4 -1
- package/lib/server/controllerMixins/renderer.js +29 -2
- package/lib/server/controllerMixins/requestDetails.js +32 -0
- package/lib/server/controllerMixins/validations.js +1 -1
- package/lib/server/doc/requestDetails.md +5 -0
- package/lib/server/server.js +1 -1
- package/package.json +1 -1
- package/test/activeRecord/afterCommitHooks.test.js +326 -0
- package/test/activeRecord/baseRecord.test.js +312 -14
- package/test/activeRecord/db/migrator.test.js +42 -0
- package/test/activeRecord/helpers/afterCommitHooks.js +47 -0
- package/test/activeRecord/helpers/baseRecordMocks.js +2 -0
- package/test/server/controllerMixins/renderer.test.js +62 -0
- package/test/server/controllerMixins/requestDetails.test.js +66 -0
|
@@ -177,6 +177,48 @@ describe('Migrator', () => {
|
|
|
177
177
|
.rejects.toThrow(`Unknown id type "${idType}". Use one of: bigint, int, uuid`);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
it ('tells the caller false is an option when refusing an id type', async () => {
|
|
181
|
+
await expect(Helpers.migrator.createTable(Helpers.randomString(), {}, { id: Helpers.randomString() }))
|
|
182
|
+
.rejects.toThrow('or false for a table that declares its own primary key');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// For a table whose primary key is a natural column, e.g. a baseline migration recreating
|
|
186
|
+
// a table that predates the migrations directory.
|
|
187
|
+
it ('adds no id column when id is false', async () => {
|
|
188
|
+
const tableName = Helpers.randomString();
|
|
189
|
+
|
|
190
|
+
await Helpers.migrator.createTable(tableName, {}, { id: false });
|
|
191
|
+
|
|
192
|
+
const [, columns] = Helpers.mockQueryInterface.createTable.mock.calls[0];
|
|
193
|
+
expect(columns).not.toHaveProperty('id');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it ('keeps the table details when id is false', async () => {
|
|
197
|
+
const tableName = Helpers.randomString();
|
|
198
|
+
const uuid = { allowNull: false, primaryKey: true, type: 'UUID' };
|
|
199
|
+
|
|
200
|
+
await Helpers.migrator.createTable(tableName, { uuid }, { id: false, timestamps: false });
|
|
201
|
+
|
|
202
|
+
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(
|
|
203
|
+
tableName, { uuid }, Helpers.noTransaction,
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const notFalseCases = [['null', null], ['zero', 0], ['an empty string', '']];
|
|
208
|
+
it.each(notFalseCases)('does not treat %s as false', async (_label, idType) => {
|
|
209
|
+
await expect(Helpers.migrator.createTable(Helpers.randomString(), {}, { id: idType }))
|
|
210
|
+
.rejects.toThrow('Unknown id type');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it ('still defaults to a bigint id when no id option is given', async () => {
|
|
214
|
+
const tableName = Helpers.randomString();
|
|
215
|
+
|
|
216
|
+
await Helpers.migrator.createTable(tableName, {}, {});
|
|
217
|
+
|
|
218
|
+
const [, columns] = Helpers.mockQueryInterface.createTable.mock.calls[0];
|
|
219
|
+
expect(columns.id).toEqual({ allowNull: false, autoIncrement: true, primaryKey: true, type: 'BIGINT' });
|
|
220
|
+
});
|
|
221
|
+
|
|
180
222
|
it ('lets the details override the id column', async () => {
|
|
181
223
|
const tableName = Helpers.randomString();
|
|
182
224
|
const id = { allowNull: false, primaryKey: true, type: 'UUID' };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const TestHelpers = require('@vida-global/test-helpers');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/***************************************************************************************************
|
|
5
|
+
* MOCKS
|
|
6
|
+
***************************************************************************************************/
|
|
7
|
+
function buildTransaction() {
|
|
8
|
+
return { afterCommit: jest.fn(), parent: null };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function buildSavepoint(parent) {
|
|
13
|
+
return { afterCommit: jest.fn(), parent };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// The callbacks module keys everything off the model class itself, so any class works as a stand in.
|
|
18
|
+
function buildModel(parent) {
|
|
19
|
+
if (parent) return class Child extends parent {};
|
|
20
|
+
|
|
21
|
+
return class Model {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
function buildRecord() {
|
|
26
|
+
return { id: TestHelpers.Faker.Text.randomString() };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/***************************************************************************************************
|
|
31
|
+
* ASSERTIONS
|
|
32
|
+
***************************************************************************************************/
|
|
33
|
+
// Nothing runs until the transaction commits, so tests drive the deferred callbacks by hand.
|
|
34
|
+
async function commit(transaction) {
|
|
35
|
+
for (const [deferred] of transaction.afterCommit.mock.calls) {
|
|
36
|
+
await deferred();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
buildModel,
|
|
43
|
+
buildRecord,
|
|
44
|
+
buildSavepoint,
|
|
45
|
+
buildTransaction,
|
|
46
|
+
commit,
|
|
47
|
+
}
|
|
@@ -494,6 +494,68 @@ describe('VidaServerController', () => {
|
|
|
494
494
|
expect(controller._toXMLElementName('first name')).toBe('first_name');
|
|
495
495
|
expect(controller._toXMLElementName('a:b')).toBe('a_b');
|
|
496
496
|
});
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
// xmlbuilder2 reads these prefixes as instructions rather than as part of the name.
|
|
500
|
+
// Sanitizing one away silently turns an intended attribute into a child element.
|
|
501
|
+
const prefixCases = [
|
|
502
|
+
['attribute', '@', '@track', 'both'],
|
|
503
|
+
['text', '#', '#text', 'value'],
|
|
504
|
+
['cdata', '$', '$cdata', 'value'],
|
|
505
|
+
['comment', '!', '!comment', 'value'],
|
|
506
|
+
['processing instruction', '?', '?target', 'value'],
|
|
507
|
+
];
|
|
508
|
+
it.each(prefixCases)('preserves the %s prefix', (_label, _prefix, key) => {
|
|
509
|
+
expect(controller._toXMLElementName(key)).toBe(key);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it ('sanitizes the name after the prefix but keeps the prefix', () => {
|
|
513
|
+
expect(controller._toXMLElementName('@my attr')).toBe('@my_attr');
|
|
514
|
+
expect(controller._toXMLElementName('@a:b')).toBe('@a_b');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it ('underscores a prefixed name that starts with a digit', () => {
|
|
518
|
+
expect(controller._toXMLElementName('@123')).toBe('@_123');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it ('returns a bare prefix untouched', () => {
|
|
522
|
+
expect(controller._toXMLElementName('@')).toBe('@');
|
|
523
|
+
expect(controller._toXMLElementName('#')).toBe('#');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it ('only treats a prefix as a prefix in the leading position', () => {
|
|
527
|
+
expect(controller._toXMLElementName('track@')).toBe('track_');
|
|
528
|
+
expect(controller._toXMLElementName('a#b')).toBe('a_b');
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
// The end-to-end reason the prefixes are preserved: without it these render as child
|
|
534
|
+
// elements (<_track>both</_track>) rather than attributes.
|
|
535
|
+
describe('#renderXMLResponse attribute rendering', () => {
|
|
536
|
+
it ('renders @-prefixed keys as XML attributes', async () => {
|
|
537
|
+
const response = buildResponseMock();
|
|
538
|
+
const controller = new FooController({query: {_format: 'xml'}}, response);
|
|
539
|
+
controller.formatResponseBody = body => body;
|
|
540
|
+
|
|
541
|
+
await controller.render({Record: {'@track': 'both', '@trim': 'do-not-trim'}});
|
|
542
|
+
|
|
543
|
+
expect(response.send).toHaveBeenCalledWith(
|
|
544
|
+
expect.stringContaining('<Record track="both" trim="do-not-trim"/>')
|
|
545
|
+
);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it ('still renders an ordinary key as a child element', async () => {
|
|
549
|
+
const response = buildResponseMock();
|
|
550
|
+
const controller = new FooController({query: {_format: 'xml'}}, response);
|
|
551
|
+
controller.formatResponseBody = body => body;
|
|
552
|
+
|
|
553
|
+
await controller.render({Record: {'weird key!': 'x'}});
|
|
554
|
+
|
|
555
|
+
expect(response.send).toHaveBeenCalledWith(
|
|
556
|
+
expect.stringContaining('<weird_key_>x</weird_key_>')
|
|
557
|
+
);
|
|
558
|
+
});
|
|
497
559
|
});
|
|
498
560
|
|
|
499
561
|
|
|
@@ -88,6 +88,72 @@ describe('VidaServerController', () => {
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
describe('#requestHost', () => {
|
|
92
|
+
it ('returns the host request header', () => {
|
|
93
|
+
request.headers.host = `${TestHelpers.Faker.Text.randomString()}.vida.dev`;
|
|
94
|
+
expect(controller.requestHost).toEqual(request.headers.host);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
describe('#forwardedProtocol', () => {
|
|
100
|
+
it ('returns null when the proxy sends no forwarded protocol', () => {
|
|
101
|
+
delete request.headers['x-forwarded-proto'];
|
|
102
|
+
expect(controller.forwardedProtocol).toBe(null);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it ('returns the forwarded protocol', () => {
|
|
106
|
+
request.headers['x-forwarded-proto'] = 'https';
|
|
107
|
+
expect(controller.forwardedProtocol).toEqual('https');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it ('returns only the first entry of a forwarded protocol chain', () => {
|
|
111
|
+
request.headers['x-forwarded-proto'] = 'https, http';
|
|
112
|
+
expect(controller.forwardedProtocol).toEqual('https');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
describe('#isProxiedRequest', () => {
|
|
118
|
+
it.each([
|
|
119
|
+
['x-forwarded-for is present', '203.0.113.7', true],
|
|
120
|
+
['x-forwarded-for is absent', undefined, false],
|
|
121
|
+
])('returns %s -> %s', (_label, forwardedFor, expected) => {
|
|
122
|
+
delete request.headers['x-forwarded-for'];
|
|
123
|
+
if (forwardedFor) request.headers['x-forwarded-for'] = forwardedFor;
|
|
124
|
+
expect(controller.isProxiedRequest).toBe(expected);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
describe('#requestUrl', () => {
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
request.headers.host = 'api.vida.dev';
|
|
132
|
+
request.originalUrl = '/things?page=2';
|
|
133
|
+
request.protocol = 'http';
|
|
134
|
+
delete request.headers['x-forwarded-proto'];
|
|
135
|
+
delete request.headers['x-forwarded-for'];
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it ('uses the forwarded protocol when the proxy sends one', () => {
|
|
139
|
+
request.headers['x-forwarded-proto'] = 'https';
|
|
140
|
+
request.headers['x-forwarded-for'] = '203.0.113.7';
|
|
141
|
+
expect(controller.requestUrl).toEqual('https://api.vida.dev/things?page=2');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Not every balancer forwards X-Forwarded-Proto, so a proxied request can look like
|
|
145
|
+
// plain http even though the caller addressed an https URL.
|
|
146
|
+
it ('assumes https for a proxied request that carries no forwarded protocol', () => {
|
|
147
|
+
request.headers['x-forwarded-for'] = '203.0.113.7';
|
|
148
|
+
expect(controller.requestUrl).toEqual('https://api.vida.dev/things?page=2');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it ('keeps the connection protocol for a direct request with no proxy headers', () => {
|
|
152
|
+
expect(controller.requestUrl).toEqual('http://api.vida.dev/things?page=2');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
|
|
91
157
|
describe('#statusCode', () => {
|
|
92
158
|
it ('returns the response status code', () => {
|
|
93
159
|
expect(controller.statusCode).toEqual(response.statusCode);
|