Skip to content

Commit

Permalink
feat: attachments (i.e. photos)
Browse files Browse the repository at this point in the history
  • Loading branch information
willruggiano committed Nov 12, 2024
1 parent ee66061 commit 4e6b8a2
Show file tree
Hide file tree
Showing 20 changed files with 837 additions and 230 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const config: CodegenConfig = {
"!*.ChecklistInProgress",
"!*.ChecklistClosed*",
"!*Payload",
"!*Geofence",
],
query: "*",
scalar: "*",
Expand Down
35 changes: 35 additions & 0 deletions lib/datasources/attachment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";
import makeAttachmentLoader from "./attachment";
import { encodeGlobalId } from "@/schema/system";

// biome-ignore lint/suspicious/noExplicitAny:
const makeLoader = () => makeAttachmentLoader({} as any);

process.env.ATTACHMENT_BUCKET = "tendrel-ruggiano-test-attachment-bucket";

describe.skipIf(!!process.env.CI)("attachment loader", () => {
test("ok", async () => {
const data = await makeLoader().byId.load(
encodeGlobalId({
type: "workpictureinstance",
id: "ace41781-58df-452b-8525-d7f1c8130586",
}),
);
expect(data).toMatchObject({
id: expect.any(String),
attachment: expect.stringMatching(
/^https:\/\/tendrel-ruggiano-test-attachment-bucket/,
),
});
});

test("not found", async () => {
const p = makeLoader().byId.load(
encodeGlobalId({
type: "__test__",
id: "1",
}),
);
expect(p).rejects.toThrow("No Attachment for id '1'");
});
});
53 changes: 53 additions & 0 deletions lib/datasources/attachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Attachment } from "@/schema";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import Dataloader from "dataloader";
import type { Request } from "express";
import { sql } from "./postgres";
import { decodeGlobalId } from "@/schema/system";
import { GraphQLError } from "graphql";

async function createPresignedUrl(client: S3Client, uri: URL) {
const command = new GetObjectCommand({
Bucket: uri.host,
Key: uri.pathname.slice(1), // remove the leading '/'
});
if (uri.protocol !== "s3:") {
throw "invariant violated";
}
return getSignedUrl(client, command, {
expiresIn: Number(process.env.ATTACHMENT_EXPIRATION_TIME_SECONDS ?? 3600),
});
}

export default (_: Request) => ({
byId: new Dataloader<string, Attachment>(async keys => {
const pks = keys.map(k => decodeGlobalId(k).id);
const rows = await sql<{ _key: string; uri: string }[]>`
SELECT
workpictureinstanceuuid AS _key,
workpictureinstancestoragelocation AS uri
FROM public.workpictureinstance
WHERE workpictureinstanceuuid IN ${sql(pks)};
`;
const s3 = new S3Client();
return Promise.all(
pks.map(async (pk, i) => {
const row = rows.find(r => r._key === pk);

if (!row) {
throw new GraphQLError(`No Attachment for id '${pk}'`, {
extensions: {
code: "NOT_FOUND",
},
});
}

return {
id: keys[i],
attachment: await createPresignedUrl(s3, new URL(row.uri)),
};
}),
);
}),
});
2 changes: 2 additions & 0 deletions lib/datasources/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import postgres, { type Fragment } from "postgres";

import type { Request } from "express";
import { makeActiveLoader } from "./activatable";
import makeAttachmentLoader from "./attachment";
import { makeAuditableLoader } from "./auditable";
import makeCustomerRequestedLanguageLoader from "./crl";
import { makeDescriptionLoader } from "./description";
Expand Down Expand Up @@ -92,6 +93,7 @@ export function unionAll(xs: readonly Fragment[]) {
export function orm(req: Request) {
return {
active: makeActiveLoader(req),
attachment: makeAttachmentLoader(req),
auditable: makeAuditableLoader(req),
crl: makeCustomerRequestedLanguageLoader(req),
description: makeDescriptionLoader(req),
Expand Down
14 changes: 8 additions & 6 deletions lib/datasources/sop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ export function makeSopLoader(_req: Request) {
"workinstance",
() => sql`
SELECT
id AS _key,
encode(('workinstance:' || id || ':sop')::bytea, 'base64') AS id,
workinstancesoplink AS sop
FROM public.workinstance
wi.id AS _key,
encode(('workinstance:' || wi.id || ':sop')::bytea, 'base64') AS id,
coalesce(wi.workinstancesoplink, wt.worktemplatesoplink) AS sop
FROM public.workinstance AS wi
INNER JOIN public.worktemplate AS wt
ON wi.workinstanceworktemplateid = wt.worktemplateid
WHERE
id IN ${sql(ids)}
AND workinstancesoplink IS NOT NULL
wi.id IN ${sql(ids)}
AND coalesce(wi.workinstancesoplink, wt.worktemplatesoplink) IS NOT null
`,
)

Expand Down
145 changes: 39 additions & 106 deletions lib/datasources/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ import type { Request } from "express";
import { match } from "ts-pattern";
import { sql, unionAll } from "./postgres";

function buildTemporalFragment(from: string) {
return sql`(
SELECT jsonb_build_object(
'__typename',
'ZonedDateTime',
'epochMilliseconds',
(extract(epoch from ${sql(from)}) * 1000)::text,
'timeZone',
tz
)
WHERE ${sql(from)} IS NOT null
)`;
}

export function makeStatusLoader(_req: Request) {
return new DataLoader<ID, ResolversTypes["ChecklistStatus"] | undefined>(
async keys => {
Expand All @@ -29,13 +43,13 @@ export function makeStatusLoader(_req: Request) {
WHEN s.systagtype = 'In Progress' THEN 'ChecklistInProgress'
ELSE 'ChecklistClosed'
END AS type,
wi.workinstancecreateddate AS opendate,
wi.workinstancestartdate AS startdate,
wi.workinstancecreateddate AS opened_at,
wi.workinstancestartdate AS in_progress_at,
CASE WHEN s.systagtype = 'Cancelled' THEN jsonb_build_object('code', 'cancel')
ELSE null
END AS closedbecause,
wi.workinstancecompleteddate AS closeddate,
wi.workinstancetargetstartdate AS duedate,
END AS closed_because,
wi.workinstancecompleteddate AS closed_at,
wi.workinstancetargetstartdate AS due_at,
wi.workinstancetimezone AS tz
FROM public.workinstance AS wi
INNER JOIN public.systag AS s
Expand All @@ -46,26 +60,8 @@ export function makeStatusLoader(_req: Request) {
SELECT
_key,
type AS "__typename",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from duedate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE duedate IS NOT null
) t
) AS "dueAt",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from opendate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE opendate IS NOT null
) t
) AS "openedAt",
${buildTemporalFragment("due_at")}::json AS "dueAt",
${buildTemporalFragment("opened_at")}::json AS "openedAt",
null::json AS "inProgressAt",
null::json AS "closedAt",
null::json AS "closedBecause"
Expand All @@ -75,27 +71,9 @@ export function makeStatusLoader(_req: Request) {
SELECT
_key,
type AS "__typename",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from duedate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE duedate IS NOT null
) t
) AS "dueAt",
null::json AS "openedAt",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from startdate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE startdate IS NOT null
) t
) AS "inProgressAt",
${buildTemporalFragment("due_at")}::json AS "dueAt",
${buildTemporalFragment("opened_at")}::json AS "openedAt",
${buildTemporalFragment("in_progress_at")}::json AS "inProgressAt",
null::json AS "closedAt",
null::json AS "closedBecause"
FROM cte
Expand All @@ -104,29 +82,11 @@ export function makeStatusLoader(_req: Request) {
SELECT
_key,
type AS "__typename",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from duedate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE duedate IS NOT null
) t
) AS "dueAt",
null::json AS "openedAt",
null::json AS "inProgressAt",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from closeddate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE closeddate IS NOT null
) t
) AS "closedAt",
closedbecause::json AS "closedBecause"
${buildTemporalFragment("due_at")}::json AS "dueAt",
${buildTemporalFragment("opened_at")}::json AS "openedAt",
${buildTemporalFragment("in_progress_at")}::json AS "inProgressAt",
${buildTemporalFragment("closed_at")}::json AS "closedAt",
closed_because::json AS "closedBecause"
FROM cte
WHERE type = 'ChecklistClosed'
)
Expand All @@ -143,10 +103,10 @@ export function makeStatusLoader(_req: Request) {
WHEN s.systagtype = 'In Progress' THEN 'ChecklistInProgress'
ELSE 'ChecklistClosed'
END AS type,
wri.workresultinstancecreateddate AS opendate,
wri.workresultinstancestartdate AS startdate,
wri.workresultinstancecompleteddate AS closeddate,
null::timestamp AS duedate,
wri.workresultinstancecreateddate AS opened_at,
wri.workresultinstancestartdate AS in_progress_at,
wri.workresultinstancecompleteddate AS closed_at,
null::timestamp AS due_at,
wi.workinstancetimezone AS tz
FROM public.workresultinstance AS wri
INNER JOIN public.workinstance AS wi
Expand All @@ -163,16 +123,7 @@ export function makeStatusLoader(_req: Request) {
_key,
type AS "__typename",
null::json AS "dueAt",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from opendate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE opendate IS NOT null
) t
) AS "openedAt",
${buildTemporalFragment("opened_at")}::json AS "openedAt",
null::json AS "inProgressAt",
null::json AS "closedAt",
null::json AS "closedBecause"
Expand All @@ -183,17 +134,8 @@ export function makeStatusLoader(_req: Request) {
_key,
type AS "__typename",
null::json AS "dueAt",
null::json AS "openedAt",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from startdate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE startdate IS NOT null
) t
) AS "inProgressAt",
${buildTemporalFragment("opened_at")}::json AS "openedAt",
${buildTemporalFragment("in_progress_at")}::json AS "inProgressAt",
null::json AS "closedAt",
null::json AS "closedBecause"
FROM cte
Expand All @@ -203,18 +145,9 @@ export function makeStatusLoader(_req: Request) {
_key,
type AS "__typename",
null::json AS "dueAt",
null::json AS "openedAt",
null::json AS "inProgressAt",
(
SELECT row_to_json(t)
FROM (
SELECT
'ZonedDateTime' AS "__typename",
(extract(epoch from closeddate) * 1000)::text AS "epochMilliseconds",
tz AS "timeZone"
WHERE closeddate IS NOT null
) t
) AS "closedAt",
${buildTemporalFragment("opened_at")}::json AS "openedAt",
${buildTemporalFragment("in_progress_at")}::json AS "inProgressAt",
${buildTemporalFragment("closed_at")}::json AS "closedAt",
null::json AS "closedBecause"
FROM cte
WHERE type = 'ChecklistClosed'
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tendrelhq/graphql",
"version": "0.15.0",
"version": "0.16.0",
"module": "schema/index.ts",
"type": "module",
"scripts": {
Expand Down Expand Up @@ -45,6 +45,8 @@
},
"dependencies": {
"@apollo/server": "^4.10.4",
"@aws-sdk/client-s3": "^3.685.0",
"@aws-sdk/s3-request-presigner": "^3.685.0",
"@clerk/clerk-sdk-node": "^5.0.12",
"@clerk/shared": "^2.3.1",
"@formatjs/intl-localematcher": "^0.5.4",
Expand Down
Loading

0 comments on commit 4e6b8a2

Please sign in to comment.