feat: regular expression and array filters, GET procedures; 0.3.0

This commit is contained in:
2026-11-02 02:09:06 +01:00
parent 8735abfe79
commit 215d1c2f7e
30 changed files with 171 additions and 44 deletions
+7
View File
@@ -66,3 +66,10 @@ CREATE FUNCTION demo.assets_tagged(tag text) RETURNS SETOF demo.assets
LANGUAGE sql STABLE AS $$
SELECT a.* FROM demo.assets a JOIN demo.asset_tags t ON t.asset_id = a.id JOIN demo.tags g ON g.id = t.tag_id
WHERE g.name = tag ORDER BY a.id $$;
CREATE TABLE demo.notes (
id integer PRIMARY KEY,
labels text[] NOT NULL
);
INSERT INTO demo.notes (id, labels) VALUES (1, '{a,b}'), (2, '{b,c}'), (3, '{z}');
+10
View File
@@ -34,3 +34,13 @@ for (const page of PAGES) {
}
});
}
describe('mysql regular expressions', { skip: !url && 'RESTLOOM_MYSQL_URL is not set' }, () => {
test('imatch ignores case, match does not', async () => {
await admin.query(fixture);
const rest = createREST({ db: mysql(pool), expose: { schemas: ['demo'] } });
const get = async (qs) => (await rest.handle({ method: 'GET', path: '/users', query: new URLSearchParams(qs), headers: {}, context: undefined })).body;
assert.deepEqual(await get('select=id&name=imatch.^(ada|LINUS)$'), [{ id: 1 }, { id: 3 }]);
assert.deepEqual(await get('select=id&name=match.^(ada|LINUS)$'), []);
});
});
+29
View File
@@ -39,3 +39,32 @@ describe('postgres procedures outside the exposed schemas', { skip: !url && 'RES
assert.equal(res.body, 3);
});
});
describe('postgres arrays, regular expressions and GET procedures', { skip: !url && 'RESTLOOM_PG_URL is not set' }, () => {
let rest;
before(async () => {
await pool.query(fixture);
rest = createREST({ db: postgres(pool), expose: { schemas: ['demo'] }, procedures: { 'tags/{tag}/assets': { function: 'demo.assets_tagged', method: 'GET' } } });
});
const get = (path, qs = '') => rest.handle({ method: 'GET', path, query: new URLSearchParams(qs), headers: {}, context: undefined });
test('array overlap, contains and contained by', async () => {
assert.deepEqual((await get('/notes', 'select=id&labels=ov.{a,z}')).body, [{ id: 1 }, { id: 3 }]);
assert.deepEqual((await get('/notes', 'select=id&labels=cs.{b}')).body, [{ id: 1 }, { id: 2 }]);
assert.deepEqual((await get('/notes', 'select=id&labels=cd.{a,b,z}')).body, [{ id: 1 }, { id: 3 }]);
assert.equal((await get('/assets', 'name=ov.{a}')).body.error, 'bad_operator');
});
test('regular expressions, with and without case', async () => {
assert.deepEqual((await get('/users', 'select=id&name=imatch.^(ada|LINUS)$')).body, [{ id: 1 }, { id: 3 }]);
assert.deepEqual((await get('/users', 'select=id&name=match.^(ada|LINUS)$')).body, []);
});
test('a GET procedure takes its arguments from the query and refuses POST', async () => {
const rows = (await get('/tags/gdpr/assets', 'select=ignored')).body;
assert.equal(rows.error, 'unknown_argument');
assert.deepEqual((await get('/tags/gdpr/assets')).body.map((r) => r.id), [2, 4]);
const post = await rest.handle({ method: 'POST', path: '/tags/gdpr/assets', query: new URLSearchParams(), headers: {}, body: {}, context: undefined });
assert.equal(post.status, 405);
});
});
+11
View File
@@ -26,3 +26,14 @@ for (const page of PAGES) {
}
});
}
describe('sqlite refuses what it cannot do', () => {
test('a regular expression answers unsupported', async () => {
const db = new Database(':memory:');
db.exec("ATTACH ':memory:' AS demo");
db.exec(fixture);
const rest = createREST({ db: sqlite(db), expose: { schemas: ['demo'] } });
const res = await rest.handle({ method: 'GET', path: '/users', query: new URLSearchParams('name=imatch.a'), headers: {}, context: undefined });
assert.deepEqual(res.body, { error: 'unsupported', message: 'regular expressions' });
});
});