Skip to content

Commit

Permalink
console refactor to its own table
Browse files Browse the repository at this point in the history
  • Loading branch information
Zeke Nierenberg committed Nov 13, 2022
1 parent 62d6aa8 commit 9968cba
Show file tree
Hide file tree
Showing 24 changed files with 649 additions and 60 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"Signin",
"slonik",
"sunglo",
"timestamptz",
"unbuilt",
"unnest",
"unsign",
Expand Down
1 change: 1 addition & 0 deletions integration-tests/migrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"license": "MIT",
"dependencies": {
"@slonik/migrator": "^0.9.0",
"lodash": "^4.17.21",
"slonik": "^27.0.0"
}
}
5 changes: 5 additions & 0 deletions integration-tests/migrator/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==

lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

minimatch@^3.0.2, minimatch@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
Expand Down
1 change: 1 addition & 0 deletions server/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ module.exports = {
preset: "ts-jest",
testPathIgnorePatterns: ["dist/"],
testEnvironment: "node",
setupFiles: ["dotenv/config"],
};
74 changes: 74 additions & 0 deletions server/migrations/2022.10.29T15.28.36.console.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const { groupBy } = require("lodash");

/** @type {import('@slonik/migrator').Migration} */
exports.up = async ({ context: { connection, sql } }) => {
await connection.query(sql`
create table "console" (
id uuid primary key default uuid_generate_v4(),
"createdAt" timestamptz not null default NOW(),
"updatedAt" timestamptz not null default NOW(),
"timestamp" timestamptz not null,
"requestId" uuid,
"stateId" uuid,
"level" varchar(25) not null,
"messages" jsonb not null default '{}',
foreign key("requestId") references request("id"),
foreign key("stateId") references state("id")
)
`);

const { rows } = await connection.query(sql`
select id, "requestId", console from "state"
where console != '[]'
`);

for (const row of rows) {
const consoleMessages = row.console;
for (const { messages, level, timestamp } of consoleMessages) {
await connection.query(sql`
insert into "console"
("timestamp", "level", "requestId", "stateId", "messages")
values
(${new Date(timestamp).toISOString()}, ${level}, ${row.requestId}, ${
row.id
}, ${sql.jsonb(messages)})
`);
}
}

await connection.query(sql`
alter table "state"
drop column console
`);
};

/** @type {import('@slonik/migrator').Migration} */
exports.down = async ({ context: { connection, sql } }) => {
await connection.query(sql`
alter table state
add column console jsonb not null default '[]'
`);

const { rows } = await connection.query(sql`
select * from "console"
`);

const consoleByState = groupBy(rows, "stateId");

for (const stateId of Object.keys(consoleByState)) {
const consoleArr = consoleByState[stateId].map((c) => ({
level: c.level,
messages: c.messages,
timestamp: new Date(c.timestamp).getTime(),
}));
await connection.query(sql`
update "state"
set "console" = ${sql.json(consoleArr)}
where id = ${stateId}
`);
}

await connection.query(sql`
drop table "console"
`);
};
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"slonik": "^28.1.0",
"slonik-interceptor-query-logging": "^1.4.2",
"typescript": "^4.5.2",
"fishery": "^2.2.2",
"uuid": "^8.3.2"
},
"devDependencies": {
Expand Down
15 changes: 10 additions & 5 deletions server/src/auth/auth.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { randomUUID } from "crypto";
import { JsonWebTokenError, sign } from "jsonwebtoken";
import {
InvalidJwtError,
InvalidJwtSubError,
InvalidOrExpiredJwtError,
} from "./auth.errors";
import { validateAndDecodeJwt } from "./auth.service";

process.env.JWT_SECRET = "secret";
Expand All @@ -18,9 +23,7 @@ describe("auth service", () => {
});
describe("validateAndDecodeJwt", () => {
it("throws with invalid jwt", () => {
expect(() => validateAndDecodeJwt(userId)).toThrowError(
"unable to decode jwt"
);
expect(() => validateAndDecodeJwt(userId)).toThrowError(InvalidJwtError);
});

it("correctly identifies a signed token", () => {
Expand All @@ -31,13 +34,15 @@ describe("auth service", () => {

it("errors when the jwt secret is incorrect", () => {
const token = makeSignedJwt(userId, "wrong secret");
expect(() => validateAndDecodeJwt(token)).toThrowError(JsonWebTokenError);
expect(() => validateAndDecodeJwt(token)).toThrowError(
InvalidOrExpiredJwtError
);
});

it("errors when the jwt subject is not a uuid", () => {
const token = makeSignedJwt("foo");
expect(() => validateAndDecodeJwt(token)).toThrowError(
"jwt sub is not uuid"
InvalidJwtSubError
);
});
});
Expand Down
73 changes: 73 additions & 0 deletions server/src/console/console.db.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import stateFactory from "../state/state.factory";
import { bulkInsertConsole, getConsoleByStateIds } from "./console.db";
import consoleFactory from "./console.factory";

describe("console.db", () => {
describe("bulkInsertConsole", () => {
it("inserts multiple console rows", async () => {
const state = await stateFactory.create();

await bulkInsertConsole({
consoleLogs: [
{
stateId: state.id,
requestId: state.requestId,
level: "warn",
messages: ["foo", "bar"],
timestamp: new Date(),
},
{
stateId: state.id,
requestId: state.requestId,
level: "warn",
messages: ["foo", "bar", "baz"],
timestamp: new Date(),
},
],
});

expect(await getConsoleByStateIds({ stateIds: [state.id] })).toEqual({
[state.id]: [
{
stateId: state.id,
requestId: state.requestId,
level: "warn",
messages: ["foo", "bar"],
timestamp: expect.any(Date),
},
{
stateId: state.id,
requestId: state.requestId,
level: "warn",
messages: ["foo", "bar", "baz"],
timestamp: expect.any(Date),
},
],
});
});
});

describe("getConsoleByStateIds", () => {
it("gets console logs by state ids", async () => {
const consoles = await consoleFactory
.params({ messages: ["foo", "bar"] })
.createList(3);
const [c, c1, c2] = consoles;
expect(
await getConsoleByStateIds({ stateIds: consoles.map((c) => c.stateId) })
).toEqual(
expect.objectContaining({
[c.stateId]: expect.arrayContaining([
expect.objectContaining({ messages: ["foo", "bar"] }),
]),
[c1.stateId]: expect.arrayContaining([
expect.objectContaining({ messages: ["foo", "bar"] }),
]),
[c2.stateId]: expect.arrayContaining([
expect.objectContaining({ messages: ["foo", "bar"] }),
]),
})
);
});
});
});
60 changes: 60 additions & 0 deletions server/src/console/console.db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { groupBy, keyBy, mapValues } from "lodash";
import { sql } from "slonik";
import { getPool } from "../db";
import { ConsoleMessage, ConsoleMessageInsert } from "./console.types";

export async function bulkInsertConsole({
consoleLogs,
}: {
consoleLogs: ConsoleMessageInsert[];
}) {
if (consoleLogs.length === 0) {
return;
}
const pool = getPool();
await pool.any(sql`
insert into "console"
("stateId", "requestId", "timestamp", "level", "messages")
select * from ${sql.unnest(
consoleLogs.map((consoleLog) => [
consoleLog.stateId,
consoleLog.requestId,
new Date(consoleLog.timestamp).toISOString(),
consoleLog.level,
JSON.stringify(consoleLog.messages || []),
]),
["uuid", "uuid", "timestamptz", "varchar", "jsonb"]
)}
returning id
`);
}

export async function getConsoleByStateIds({
stateIds,
}: {
stateIds: string[];
}) {
const consoleRecords = (
await getPool().any<ConsoleMessage>(sql`
select
messages,
"requestId",
"stateId",
timestamp,
level
from "console"
where "stateId" = ANY(${sql.array(stateIds, "uuid")})
`)
).map((r) => ({ ...r, timestamp: new Date(r.timestamp) }));

return groupBy(consoleRecords, "stateId");
}

export async function getConsoleByStateId({
stateId,
}: {
stateId: string;
}): Promise<ConsoleMessageInsert[]> {
const map = await getConsoleByStateIds({ stateIds: [stateId] });
return map[stateId] || [];
}
41 changes: 41 additions & 0 deletions server/src/console/console.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Factory } from "fishery";
import { sql } from "slonik";
import { getPool } from "../db";
import stateFactory from "../state/state.factory";
import { ConsoleRow } from "./console.types";
import crypto from "crypto";

export default Factory.define<ConsoleRow>(
({ associations, onCreate, params }) => {
onCreate(async (row) => {
if (!row.stateId) {
const state = await stateFactory.create();
row.stateId = state.id;
row.requestId = state.requestId;
}
await getPool().one(sql`
insert into "console"
(id, "level", messages, "timestamp", "requestId", "stateId")
values
(
${row.id},
${row.level},
${sql.jsonb(row.messages)},
${row.timestamp.toISOString()},
${row.requestId!},
${row.stateId!}
)
returning id
`);
return row;
});
return {
id: crypto.randomUUID(),
level: "log",
messages: params.messages || [],
timestamp: new Date(),
requestId: associations.requestId,
stateId: associations.stateId,
} as ConsoleRow;
}
);
21 changes: 21 additions & 0 deletions server/src/console/console.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type LogLevels = "warn" | "error" | "log" | "trace" | "debug" | "info";

export type ConsoleMessageInsert = {
stateId?: string;
} & ConsoleMessage;

export type ConsoleMessage = {
level: LogLevels;
messages: string[];
timestamp: Date;
requestId?: string;
};

export type ConsoleRow = {
id: string;
level: LogLevels;
messages: string[];
timestamp: Date;
requestId?: string;
stateId: string;
};
Loading

0 comments on commit 9968cba

Please sign in to comment.