Skip to content

Commit

Permalink
feat: Match the new Turborepo OpenAPI spec (#394)
Browse files Browse the repository at this point in the history
  • Loading branch information
AdiRishi committed Apr 16, 2024
1 parent 374babd commit 2005a64
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 58 deletions.
7 changes: 7 additions & 0 deletions .changeset/eight-horses-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'turborepo-remote-cache-cf': minor
---

Update routes to conform to the new Turborepo OpenAPI spec.

Overall this is not a breaking change as most additions are optional.
4 changes: 2 additions & 2 deletions .dev.vars
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ENVIRONMENT = 'development'
TURBO_TOKEN = 'SECRET'
ENVIRONMENT=development
TURBO_TOKEN=SECRET
110 changes: 86 additions & 24 deletions src/routes/v8/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { bearerAuth } from 'hono/bearer-auth';
import { cache } from 'hono/cache';
import { z } from 'zod';

export const DEFAULT_TEAM_ID = 'team_default_team';

// Route - /v8/artifacts
export const artifactRouter = new Hono<{ Bindings: Env }>();

Expand All @@ -13,63 +15,94 @@ artifactRouter.use('*', async (c, next) => {
await middleware(c, next);
});

artifactRouter.post(
'/',
zValidator(
'json',
z.object({
hashes: z.array(z.string()), // artifactIds
})
),
zValidator('query', z.object({ teamId: z.string().optional(), slug: z.string().optional() })),
(c) => {
const data = c.req.valid('json');
const { teamId: teamIdQuery, slug } = c.req.valid('query');
const teamId = teamIdQuery ?? slug ?? DEFAULT_TEAM_ID;
void data;
void teamId;
// TODO: figure out what this route actually does, the OpenAPI spec is unclear
return c.json({});
}
);

artifactRouter.get('/status', (c) => {
const status: 'disabled' | 'enabled' | 'over_limit' | 'paused' = 'enabled';
return c.json({ status }, 200);
});

artifactRouter.put(
'/:artifactId',
zValidator('param', z.object({ artifactId: z.string() })),
zValidator('query', z.object({ teamId: z.string().optional(), slug: z.string().optional() })),
zValidator(
'header',
z.object({
'content-type': z.literal('application/octet-stream'),
'content-length': z.coerce.number().optional(),
'x-artifact-duration': z.coerce.number().optional(),
'x-artifact-client-ci': z.string().optional(),
'x-artifact-client-interactive': z.coerce.number().min(0).max(1).optional(),
'x-artifact-tag': z.string().optional(),
})
),
async (c) => {
const { artifactId } = c.req.valid('param');
const { teamId: teamIdQuery, slug } = c.req.valid('query');
const teamId = teamIdQuery ?? slug;

if (!teamId) {
return c.json({ error: 'MISSING_TEAM_ID' }, 400);
}
const teamId = teamIdQuery ?? slug ?? DEFAULT_TEAM_ID;
const validatedHeaders = c.req.valid('header');

const contentType = c.req.raw.headers.get('Content-Type');
if (contentType !== 'application/octet-stream') {
return c.json({ error: 'EXPECTED_CONTENT_TYPE_OCTET_STREAM' }, 400);
}

// if present the turborepo client has signed the artifact body
const artifactTag = c.req.raw.headers.get('x-artifact-tag');
const storage = c.env.STORAGE_MANAGER.getActiveStorage();
const objectKey = `${teamId}/${artifactId}`;

const storageMetadata: Record<string, string> = {};
if (artifactTag) {
storageMetadata.artifactTag = artifactTag;
if (validatedHeaders['x-artifact-tag']) {
storageMetadata.artifactTag = validatedHeaders['x-artifact-tag'];
}
await storage.write(objectKey, c.req.raw.body!, storageMetadata);

return c.json({ teamId, artifactId, storagePath: objectKey }, 201);
const uploadUrl = new URL(`${artifactId}?teamId=${teamId}`, c.req.raw.url).toString();
return c.json({ urls: [uploadUrl] }, 202);
}
);

// Hono router .get() method captures both GET and HEAD requests
artifactRouter.get(
'/:artifactId/:teamId?',
'/:artifactId',
cache({
cacheName: 'r2-artifacts',
wait: false,
cacheControl: 'max-age=300, stale-while-revalidate=300',
}),
zValidator('param', z.object({ artifactId: z.string() })),
zValidator('query', z.object({ teamId: z.string().optional(), slug: z.string().optional() })),
zValidator(
'header',
z.object({
'x-artifact-client-ci': z.string().optional(),
'x-artifact-client-interactive': z.coerce.number().min(0).max(1).optional(),
})
),
async (c) => {
const { artifactId } = c.req.valid('param');
const { teamId: teamIdQuery, slug } = c.req.valid('query');
const teamId = teamIdQuery ?? slug;
const teamId = teamIdQuery ?? slug ?? DEFAULT_TEAM_ID;

if (!teamId) {
return c.json({ error: 'MISSING_TEAM_ID' }, 400);
}
const storage = c.env.STORAGE_MANAGER.getActiveStorage();
const objectKey = `${teamId}/${artifactId}`;

const storedObject = await storage.readWithMetadata(objectKey);
if (!storedObject.data) {
return c.json({ error: 'NOT_FOUND' }, 404);
return c.json({}, 404);
}

c.header('Content-Type', 'application/octet-stream');
Expand All @@ -81,6 +114,35 @@ artifactRouter.get(
}
);

artifactRouter.post('/events', (c) => {
return c.json({});
});
artifactRouter.post(
'/events',
zValidator(
'json',
z.array(
z.object({
sessionId: z.string().uuid(),
source: z.union([z.literal('LOCAL'), z.literal('REMOTE')]),
event: z.union([z.literal('HIT'), z.literal('MISS')]),
hash: z.string(), // artifactId
duration: z.coerce.number().optional(),
})
)
),
zValidator('query', z.object({ teamId: z.string().optional(), slug: z.string().optional() })),
zValidator(
'header',
z.object({
'x-artifact-client-ci': z.string().optional(),
'x-artifact-client-interactive': z.coerce.number().min(0).max(1).optional(),
})
),
(c) => {
const data = c.req.valid('json');
const { teamId: teamIdQuery, slug } = c.req.valid('query');
const teamId = teamIdQuery ?? slug ?? DEFAULT_TEAM_ID;
// TODO: track these events and store them to query later
void data;
void teamId;
return c.json({});
}
);
Loading

0 comments on commit 2005a64

Please sign in to comment.