Skip to content

Commit

Permalink
Replace zod with superstruct
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown committed May 8, 2024
1 parent 8c8bcb5 commit 9be94b3
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 155 deletions.
4 changes: 2 additions & 2 deletions packages/keystatic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,12 @@
"slate": "^0.91.4",
"slate-history": "^0.86.0",
"slate-react": "^0.91.9",
"superstruct": "^1.0.4",
"unist-util-visit": "^5.0.0",
"urql": "^4.0.0",
"y-prosemirror": "^1.2.2",
"y-protocols": "^1.0.6",
"yjs": "^13.6.11",
"zod": "^3.20.2"
"yjs": "^13.6.11"
},
"devDependencies": {
"@jest/expect": "^29.7.0",
Expand Down
67 changes: 36 additions & 31 deletions packages/keystatic/src/api/api-node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'node:path';
import { z } from 'zod';
import * as s from 'superstruct';
import fs from 'node:fs/promises';
import { Config } from '../config';
import {
Expand All @@ -10,6 +10,7 @@ import {
import { readToDirEntries, getAllowedDirectories } from './read-local';
import { blobSha } from '../app/trees';
import { randomBytes } from 'node:crypto';
import { base64UrlDecode } from '#base64';

// this should be trivially dead code eliminated
// it's just to ensure the types are exactly the same between this and local-noop.ts
Expand All @@ -23,10 +24,10 @@ function _typeTest() {
let _d: typeof b = a;
}

const ghAppSchema = z.object({
slug: z.string(),
client_id: z.string(),
client_secret: z.string(),
const ghAppSchema = s.type({
slug: s.string(),
client_id: s.string(),
client_secret: s.string(),
});

const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Expand Down Expand Up @@ -55,21 +56,21 @@ export async function handleGitHubAppCreation(
};
}
const ghAppDataRaw = await ghAppRes.json();

const ghAppDataResult = ghAppSchema.safeParse(ghAppDataRaw);

if (!ghAppDataResult.success) {
let ghAppDataResult;
try {
ghAppDataResult = s.create(ghAppDataRaw, ghAppSchema);
} catch {
console.log(ghAppDataRaw);
return {
status: 500,
body: 'An unexpected response was received from GitHub',
};
}
const toAddToEnv = `# Keystatic
KEYSTATIC_GITHUB_CLIENT_ID=${ghAppDataResult.data.client_id}
KEYSTATIC_GITHUB_CLIENT_SECRET=${ghAppDataResult.data.client_secret}
KEYSTATIC_GITHUB_CLIENT_ID=${ghAppDataResult.client_id}
KEYSTATIC_GITHUB_CLIENT_SECRET=${ghAppDataResult.client_secret}
KEYSTATIC_SECRET=${randomBytes(40).toString('hex')}
${slugEnvVarName ? `${slugEnvVarName}=${ghAppDataResult.data.slug}\n` : ''}`;
${slugEnvVarName ? `${slugEnvVarName}=${ghAppDataResult.slug}\n` : ''}`;

let prevEnv: string | undefined;
try {
Expand All @@ -80,9 +81,7 @@ ${slugEnvVarName ? `${slugEnvVarName}=${ghAppDataResult.data.slug}\n` : ''}`;
const newEnv = prevEnv ? `${prevEnv}\n\n${toAddToEnv}` : toAddToEnv;
await fs.writeFile('.env', newEnv);
await wait(200);
return redirect(
'/keystatic/created-github-app?slug=' + ghAppDataResult.data.slug
);
return redirect('/keystatic/created-github-app?slug=' + ghAppDataResult.slug);
}

export function localModeApiHandler(
Expand Down Expand Up @@ -165,6 +164,10 @@ async function blob(
return { status: 200, body: contents };
}

const base64Schema = s.coerce(s.instance(Uint8Array), s.string(), val =>
base64UrlDecode(val)
);

async function update(
req: KeystaticRequest,
config: Config,
Expand All @@ -177,24 +180,26 @@ async function update(
return { status: 400, body: 'Bad Request' };
}
const isFilepathValid = getIsPathValid(config);
const filepath = s.refine(s.string(), 'filepath', isFilepathValid);
let updates;

const updates = z
.object({
additions: z.array(
z.object({
path: z.string().refine(isFilepathValid),
contents: z.string().transform(x => Buffer.from(x, 'base64')),
})
),
deletions: z.array(
z.object({ path: z.string().refine(isFilepathValid) })
),
})
.safeParse(await req.json());
if (!updates.success) {
try {
updates = s.create(
await req.json(),
s.object({
additions: s.array(
s.object({
path: filepath,
contents: base64Schema,
})
),
deletions: s.array(s.object({ path: filepath })),
})
);
} catch {
return { status: 400, body: 'Bad data' };
}
for (const addition of updates.data.additions) {
for (const addition of updates.additions) {
await fs.mkdir(path.dirname(path.join(baseDirectory, addition.path)), {
recursive: true,
});
Expand All @@ -203,7 +208,7 @@ async function update(
addition.contents
);
}
for (const deletion of updates.data.deletions) {
for (const deletion of updates.deletions) {
await fs.rm(path.join(baseDirectory, deletion.path), { force: true });
}
return {
Expand Down
34 changes: 19 additions & 15 deletions packages/keystatic/src/api/generic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cookie from 'cookie';
import z from 'zod';
import * as s from 'superstruct';
import { Config } from '..';
import {
KeystaticResponse,
Expand Down Expand Up @@ -181,13 +181,13 @@ export function makeGenericAPIRouteHandler(
};
}

const tokenDataResultType = z.object({
access_token: z.string(),
expires_in: z.number(),
refresh_token: z.string(),
refresh_token_expires_in: z.number(),
scope: z.string(),
token_type: z.literal('bearer'),
const tokenDataResultType = s.type({
access_token: s.string(),
expires_in: s.number(),
refresh_token: s.string(),
refresh_token_expires_in: s.number(),
scope: s.string(),
token_type: s.literal('bearer'),
});

async function githubOauthCallback(
Expand Down Expand Up @@ -231,12 +231,14 @@ async function githubOauthCallback(
return { status: 401, body: 'Authorization failed' };
}
const _tokenData = await tokenRes.json();
const tokenDataParseResult = tokenDataResultType.safeParse(_tokenData);
if (!tokenDataParseResult.success) {
let tokenData;
try {
tokenData = tokenDataResultType.create(_tokenData);
} catch {
return { status: 401, body: 'Authorization failed' };
}

const headers = await getTokenCookies(tokenDataParseResult.data, config);
const headers = await getTokenCookies(tokenData, config);
if (state === 'close') {
return {
headers: [...headers, ['Content-Type', 'text/html']],
Expand All @@ -248,7 +250,7 @@ async function githubOauthCallback(
}

async function getTokenCookies(
tokenData: z.infer<typeof tokenDataResultType>,
tokenData: s.Infer<typeof tokenDataResultType>,
config: InnerAPIRouteConfig
) {
const headers: [string, string][] = [
Expand Down Expand Up @@ -332,11 +334,13 @@ async function refreshGitHubAuth(
return;
}
const _tokenData = await tokenRes.json();
const tokenDataParseResult = tokenDataResultType.safeParse(_tokenData);
if (!tokenDataParseResult.success) {
let tokenData;
try {
tokenData = tokenDataResultType.create(_tokenData);
} catch {
return;
}
return getTokenCookies(tokenDataParseResult.data, config);
return getTokenCookies(tokenData, config);
}

async function githubRepoNotFound(
Expand Down
18 changes: 9 additions & 9 deletions packages/keystatic/src/app/ItemPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useState,
} from 'react';
import * as Y from 'yjs';
import { z } from 'zod';
import * as s from 'superstruct';

import { ActionGroup, Item } from '@keystar/ui/action-group';
import { Badge } from '@keystar/ui/badge';
Expand Down Expand Up @@ -95,12 +95,12 @@ type ItemPageProps = {
basePath: string;
};

const storedValSchema = z.object({
version: z.literal(1),
savedAt: z.date(),
slug: z.string(),
beforeTreeKey: z.string(),
files: z.map(z.string(), z.instanceof(Uint8Array)),
const storedValSchema = s.type({
version: s.literal(1),
savedAt: s.date(),
slug: s.string(),
beforeTreeKey: s.string(),
files: s.map(s.string(), s.instance(Uint8Array)),
});

function ItemPageInner(
Expand Down Expand Up @@ -433,7 +433,7 @@ function LocalItemPage(
state,
});
const files = new Map(serialized.map(x => [x.path, x.contents]));
const data: z.infer<typeof storedValSchema> = {
const data: s.Infer<typeof storedValSchema> = {
beforeTreeKey: localTreeKey,
slug,
files,
Expand Down Expand Up @@ -837,7 +837,7 @@ function ItemPageWrapper(props: ItemPageWrapperProps) {
props.itemSlug,
]);
if (!raw) throw new Error('No draft found');
const stored = storedValSchema.parse(raw);
const stored = storedValSchema.create(raw);
const parsed = parseEntry(
{
config: props.config,
Expand Down
16 changes: 8 additions & 8 deletions packages/keystatic/src/app/SingletonPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {
setDraft,
showDraftRestoredToast,
} from './persistence';
import { z } from 'zod';
import * as s from 'superstruct';
import { LOADING, useData } from './useData';
import { ActionGroup, Item } from '@keystar/ui/action-group';
import { useMediaQuery, breakpointQueries } from '@keystar/ui/style';
Expand Down Expand Up @@ -356,7 +356,7 @@ function LocalSingletonPage(
state,
});
const files = new Map(serialized.map(x => [x.path, x.contents]));
const data: z.infer<typeof storedValSchema> = {
const data: s.Infer<typeof storedValSchema> = {
beforeTreeKey: localTreeKey,
files,
savedAt: new Date(),
Expand Down Expand Up @@ -488,11 +488,11 @@ function CollabSingletonPage(
);
}

const storedValSchema = z.object({
version: z.literal(1),
savedAt: z.date(),
beforeTreeKey: z.string().optional(),
files: z.map(z.string(), z.instanceof(Uint8Array)),
const storedValSchema = s.type({
version: s.literal(1),
savedAt: s.date(),
beforeTreeKey: s.optional(s.string()),
files: s.map(s.string(), s.instance(Uint8Array)),
});

function SingletonPageWrapper(props: { singleton: string; config: Config }) {
Expand All @@ -516,7 +516,7 @@ function SingletonPageWrapper(props: { singleton: string; config: Config }) {
useCallback(async () => {
const raw = await getDraft(['singleton', props.singleton]);
if (!raw) throw new Error('No draft found');
const stored = storedValSchema.parse(raw);
const stored = storedValSchema.create(raw);
const parsed = parseEntry(
{
config: props.config,
Expand Down
12 changes: 6 additions & 6 deletions packages/keystatic/src/app/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { parse } from 'cookie';
import { z } from 'zod';
import * as s from 'superstruct';
import { Config } from '../config';

const storedTokenSchema = z.object({
token: z.string(),
project: z.string(),
validUntil: z.number().transform(val => new Date(val)),
const storedTokenSchema = s.object({
token: s.string(),
project: s.string(),
validUntil: s.coerce(s.date(), s.number(), val => new Date(val)),
});

export function getSyncAuth(config: Config) {
Expand Down Expand Up @@ -33,7 +33,7 @@ export function getCloudAuth(config: Config) {
);
let tokenData;
try {
tokenData = storedTokenSchema.parse(JSON.parse(unparsedTokenData!));
tokenData = storedTokenSchema.create(JSON.parse(unparsedTokenData!));
} catch (err) {
return null;
}
Expand Down
Loading

0 comments on commit 9be94b3

Please sign in to comment.