Skip to content

Commit

Permalink
Allow for passing multiple effects to triggers (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
leo authored Nov 6, 2024
1 parent 4ffb01b commit a70561e
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 50 deletions.
24 changes: 16 additions & 8 deletions src/utils/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ const SYSTEM_SCHEMAS: Array<Schema> = [
{ slug: 'schema', type: 'reference', target: { slug: 'schema' }, required: true },
{ slug: 'cause', type: 'string', required: true },
{ slug: 'filter', type: 'json' },
{ slug: 'effect', type: 'json', required: true },
{ slug: 'effects', type: 'json', required: true },
],
},
];
Expand Down Expand Up @@ -599,7 +599,7 @@ export const addSchemaQueries = (
const statementParts: Array<string> = [cause, 'ON', `"${tableName}"`];

// The query that will be executed when the trigger is fired.
const effectQuery: Query = instructionList?.effect;
const effectQueries: Array<Query> = instructionList?.effects;

// The query instructions that are used to determine whether the trigger should be
// fired, or not.
Expand All @@ -608,7 +608,10 @@ export const addSchemaQueries = (
// If filtering instructions were defined, or if the effect query references
// specific record fields, that means the trigger must be executed on a per-record
// basis, meaning "for each row", instead of on a per-query basis.
if (filterQuery || findInObject(effectQuery, RONIN_SCHEMA_SYMBOLS.FIELD)) {
if (
filterQuery ||
effectQueries.some((query) => findInObject(query, RONIN_SCHEMA_SYMBOLS.FIELD))
) {
statementParts.push('FOR EACH ROW');
}

Expand All @@ -632,13 +635,18 @@ export const addSchemaQueries = (
statementParts.push('WHEN', `(${withStatement})`);
}

// Compile the effect query into an SQL statement.
const { readStatement: effectStatement } = compileQueryInput(effectQuery, schemas, {
statementValues,
disableReturning: true,
// Compile the effect queries into SQL statements.
const effectStatements = effectQueries.map((effectQuery) => {
return compileQueryInput(effectQuery, schemas, {
statementValues,
disableReturning: true,
}).readStatement;
});

statementParts.push('BEGIN', effectStatement);
if (effectStatements.length > 1) statementParts.push('BEGIN');
statementParts.push(effectStatements.join('; '));
if (effectStatements.length > 1) statementParts.push('END');

statement += ` ${statementParts.join(' ')}`;
}

Expand Down
180 changes: 138 additions & 42 deletions tests/meta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,15 +422,17 @@ test('drop existing index', () => {
});

test('create new trigger for creating records', () => {
const triggerQuery = {
create: {
signup: {
to: {
year: 2000,
const effectQueries = [
{
create: {
signup: {
to: {
year: 2000,
},
},
},
},
};
];

const query: Query = {
create: {
Expand All @@ -439,7 +441,7 @@ test('create new trigger for creating records', () => {
slug: 'trigger_name',
schema: { slug: 'account' },
cause: 'afterInsert',
effect: triggerQuery,
effects: effectQueries,
},
},
},
Expand All @@ -458,11 +460,11 @@ test('create new trigger for creating records', () => {
const { writeStatements, readStatement, values } = compileQueryInput(query, schemas);

expect(writeStatements).toEqual([
'CREATE TRIGGER "trigger_name" AFTER INSERT ON "accounts" BEGIN INSERT INTO "signups" ("year", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?1, ?2, ?3, ?4)',
'CREATE TRIGGER "trigger_name" AFTER INSERT ON "accounts" INSERT INTO "signups" ("year", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?1, ?2, ?3, ?4)',
]);

expect(readStatement).toBe(
'INSERT INTO "triggers" ("slug", "schema", "cause", "effect", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?5, (SELECT "id" FROM "schemas" WHERE ("slug" = ?6) LIMIT 1), ?7, IIF("effect" IS NULL, ?8, json_patch("effect", ?8)), ?9, ?10, ?11) RETURNING *',
'INSERT INTO "triggers" ("slug", "schema", "cause", "effects", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?5, (SELECT "id" FROM "schemas" WHERE ("slug" = ?6) LIMIT 1), ?7, IIF("effects" IS NULL, ?8, json_patch("effects", ?8)), ?9, ?10, ?11) RETURNING *',
);

expect(values[0]).toBe(2000);
Expand All @@ -477,7 +479,7 @@ test('create new trigger for creating records', () => {
expect(values[4]).toBe('trigger_name');
expect(values[5]).toBe('account');
expect(values[6]).toBe('afterInsert');
expect(values[7]).toBe(JSON.stringify(triggerQuery));
expect(values[7]).toBe(JSON.stringify(effectQueries));
expect(values[8]).toMatch(RECORD_ID_REGEX);
expect(values[9]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
Expand All @@ -487,27 +489,117 @@ test('create new trigger for creating records', () => {
);
});

test('create new per-record trigger for creating records', () => {
const triggerQuery = {
test('create new trigger for creating records with multiple effects', () => {
const effectQueries = [
{
create: {
signup: {
to: {
year: 2000,
},
},
},
},
{
create: {
candidate: {
to: {
year: 2020,
},
},
},
},
];

const query: Query = {
create: {
member: {
trigger: {
to: {
account: `${RONIN_SCHEMA_SYMBOLS.FIELD_NEW}createdBy`,
role: 'owner',
pending: false,
slug: 'trigger_name',
schema: { slug: 'account' },
cause: 'afterInsert',
effects: effectQueries,
},
},
},
};

const schemas: Array<Schema> = [
{
slug: 'candidate',
fields: [{ slug: 'year', type: 'number' }],
},
{
slug: 'signup',
fields: [{ slug: 'year', type: 'number' }],
},
{
slug: 'account',
},
];

const { writeStatements, readStatement, values } = compileQueryInput(query, schemas);

expect(writeStatements).toEqual([
'CREATE TRIGGER "trigger_name" AFTER INSERT ON "accounts" BEGIN INSERT INTO "signups" ("year", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?1, ?2, ?3, ?4); INSERT INTO "candidates" ("year", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?5, ?6, ?7, ?8) END',
]);

expect(readStatement).toBe(
'INSERT INTO "triggers" ("slug", "schema", "cause", "effects", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?9, (SELECT "id" FROM "schemas" WHERE ("slug" = ?10) LIMIT 1), ?11, IIF("effects" IS NULL, ?12, json_patch("effects", ?12)), ?13, ?14, ?15) RETURNING *',
);

expect(values[0]).toBe(2000);
expect(values[1]).toMatch(RECORD_ID_REGEX);
expect(values[2]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
);
expect(values[3]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
);
expect(values[4]).toBe(2020);
expect(values[5]).toMatch(RECORD_ID_REGEX);
expect(values[6]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
);
expect(values[7]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
);
expect(values[8]).toBe('trigger_name');
expect(values[9]).toBe('account');
expect(values[10]).toBe('afterInsert');
expect(values[11]).toBe(JSON.stringify(effectQueries));
expect(values[12]).toMatch(RECORD_ID_REGEX);
expect(values[13]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
);
expect(values[14]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
);
});

test('create new per-record trigger for creating records', () => {
const effectQueries = [
{
create: {
member: {
to: {
account: `${RONIN_SCHEMA_SYMBOLS.FIELD_NEW}createdBy`,
role: 'owner',
pending: false,
},
},
},
},
];

const query: Query = {
create: {
trigger: {
to: {
slug: 'trigger_name',
schema: { slug: 'team' },
cause: 'afterInsert',
effect: triggerQuery,
effects: effectQueries,
},
},
},
Expand All @@ -533,11 +625,11 @@ test('create new per-record trigger for creating records', () => {
const { writeStatements, readStatement, values } = compileQueryInput(query, schemas);

expect(writeStatements).toEqual([
'CREATE TRIGGER "trigger_name" AFTER INSERT ON "teams" FOR EACH ROW BEGIN INSERT INTO "members" ("account", "role", "pending", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (NEW."createdBy", ?1, ?2, ?3, ?4, ?5)',
'CREATE TRIGGER "trigger_name" AFTER INSERT ON "teams" FOR EACH ROW INSERT INTO "members" ("account", "role", "pending", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (NEW."createdBy", ?1, ?2, ?3, ?4, ?5)',
]);

expect(readStatement).toBe(
'INSERT INTO "triggers" ("slug", "schema", "cause", "effect", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?6, (SELECT "id" FROM "schemas" WHERE ("slug" = ?7) LIMIT 1), ?8, IIF("effect" IS NULL, ?9, json_patch("effect", ?9)), ?10, ?11, ?12) RETURNING *',
'INSERT INTO "triggers" ("slug", "schema", "cause", "effects", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?6, (SELECT "id" FROM "schemas" WHERE ("slug" = ?7) LIMIT 1), ?8, IIF("effects" IS NULL, ?9, json_patch("effects", ?9)), ?10, ?11, ?12) RETURNING *',
);

expect(values[0]).toBe('owner');
Expand All @@ -553,7 +645,7 @@ test('create new per-record trigger for creating records', () => {
expect(values[5]).toBe('trigger_name');
expect(values[6]).toBe('team');
expect(values[7]).toBe('afterInsert');
expect(values[8]).toBe(JSON.stringify(triggerQuery));
expect(values[8]).toBe(JSON.stringify(effectQueries));
expect(values[9]).toMatch(RECORD_ID_REGEX);
expect(values[10]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
Expand All @@ -564,15 +656,17 @@ test('create new per-record trigger for creating records', () => {
});

test('create new per-record trigger for deleting records', () => {
const triggerQuery = {
drop: {
members: {
with: {
account: `${RONIN_SCHEMA_SYMBOLS.FIELD_OLD}createdBy`,
const effectQueries = [
{
drop: {
members: {
with: {
account: `${RONIN_SCHEMA_SYMBOLS.FIELD_OLD}createdBy`,
},
},
},
},
};
];

const query: Query = {
create: {
Expand All @@ -581,7 +675,7 @@ test('create new per-record trigger for deleting records', () => {
slug: 'trigger_name',
schema: { slug: 'team' },
cause: 'afterDelete',
effect: triggerQuery,
effects: effectQueries,
},
},
},
Expand All @@ -607,17 +701,17 @@ test('create new per-record trigger for deleting records', () => {
const { writeStatements, readStatement, values } = compileQueryInput(query, schemas);

expect(writeStatements).toEqual([
'CREATE TRIGGER "trigger_name" AFTER DELETE ON "teams" FOR EACH ROW BEGIN DELETE FROM "members" WHERE ("account" = OLD."createdBy")',
'CREATE TRIGGER "trigger_name" AFTER DELETE ON "teams" FOR EACH ROW DELETE FROM "members" WHERE ("account" = OLD."createdBy")',
]);

expect(readStatement).toBe(
'INSERT INTO "triggers" ("slug", "schema", "cause", "effect", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?1, (SELECT "id" FROM "schemas" WHERE ("slug" = ?2) LIMIT 1), ?3, IIF("effect" IS NULL, ?4, json_patch("effect", ?4)), ?5, ?6, ?7) RETURNING *',
'INSERT INTO "triggers" ("slug", "schema", "cause", "effects", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?1, (SELECT "id" FROM "schemas" WHERE ("slug" = ?2) LIMIT 1), ?3, IIF("effects" IS NULL, ?4, json_patch("effects", ?4)), ?5, ?6, ?7) RETURNING *',
);

expect(values[0]).toBe('trigger_name');
expect(values[1]).toBe('team');
expect(values[2]).toBe('afterDelete');
expect(values[3]).toBe(JSON.stringify(triggerQuery));
expect(values[3]).toBe(JSON.stringify(effectQueries));
expect(values[4]).toMatch(RECORD_ID_REGEX);
expect(values[5]).toSatisfy(
(value) => typeof value === 'string' && typeof Date.parse(value) === 'number',
Expand All @@ -628,17 +722,19 @@ test('create new per-record trigger for deleting records', () => {
});

test('create new per-record trigger with filters for creating records', () => {
const triggerQuery = {
create: {
member: {
to: {
account: `${RONIN_SCHEMA_SYMBOLS.FIELD_NEW}createdBy`,
role: 'owner',
pending: false,
const effectQueries = [
{
create: {
member: {
to: {
account: `${RONIN_SCHEMA_SYMBOLS.FIELD_NEW}createdBy`,
role: 'owner',
pending: false,
},
},
},
},
};
];

const filterInstruction = {
handle: {
Expand All @@ -653,7 +749,7 @@ test('create new per-record trigger with filters for creating records', () => {
slug: 'trigger_name',
schema: { slug: 'team' },
cause: 'afterInsert',
effect: triggerQuery,
effects: effectQueries,
filter: filterInstruction,
},
},
Expand Down Expand Up @@ -681,11 +777,11 @@ test('create new per-record trigger with filters for creating records', () => {
const { writeStatements, readStatement, values } = compileQueryInput(query, schemas);

expect(writeStatements).toEqual([
'CREATE TRIGGER "trigger_name" AFTER INSERT ON "teams" FOR EACH ROW WHEN ((NEW."handle" LIKE %?1)) BEGIN INSERT INTO "members" ("account", "role", "pending", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (NEW."createdBy", ?2, ?3, ?4, ?5, ?6)',
'CREATE TRIGGER "trigger_name" AFTER INSERT ON "teams" FOR EACH ROW WHEN ((NEW."handle" LIKE %?1)) INSERT INTO "members" ("account", "role", "pending", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (NEW."createdBy", ?2, ?3, ?4, ?5, ?6)',
]);

expect(readStatement).toBe(
'INSERT INTO "triggers" ("slug", "schema", "cause", "effect", "filter", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?7, (SELECT "id" FROM "schemas" WHERE ("slug" = ?8) LIMIT 1), ?9, IIF("effect" IS NULL, ?10, json_patch("effect", ?10)), IIF("filter" IS NULL, ?11, json_patch("filter", ?11)), ?12, ?13, ?14) RETURNING *',
'INSERT INTO "triggers" ("slug", "schema", "cause", "effects", "filter", "id", "ronin.createdAt", "ronin.updatedAt") VALUES (?7, (SELECT "id" FROM "schemas" WHERE ("slug" = ?8) LIMIT 1), ?9, IIF("effects" IS NULL, ?10, json_patch("effects", ?10)), IIF("filter" IS NULL, ?11, json_patch("filter", ?11)), ?12, ?13, ?14) RETURNING *',
);

expect(values[0]).toBe('_hidden');
Expand All @@ -702,7 +798,7 @@ test('create new per-record trigger with filters for creating records', () => {
expect(values[6]).toBe('trigger_name');
expect(values[7]).toBe('team');
expect(values[8]).toBe('afterInsert');
expect(values[9]).toBe(JSON.stringify(triggerQuery));
expect(values[9]).toBe(JSON.stringify(effectQueries));
expect(values[10]).toBe(JSON.stringify(filterInstruction));
expect(values[11]).toMatch(RECORD_ID_REGEX);
expect(values[12]).toSatisfy(
Expand Down

0 comments on commit a70561e

Please sign in to comment.