Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions client/packages/admin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ import {
validateQuery,
validateTransactions,
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
SSEConnection,
InstantStream,
EventSourceConstructor,
Expand Down Expand Up @@ -1621,6 +1626,11 @@ export {
lookup,
i,
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
Webhooks,
WebhooksManager,
type WebhookAction,
Expand Down
64 changes: 64 additions & 0 deletions client/packages/core/__tests__/src/createRouteHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { expect, test } from 'vitest';
import {
createInstantRouteHandler,
createInstantRouteHandlerBody,
type User,
} from '../../src';

const user: User = {
id: 'user-id',
refresh_token: 'refresh-token',
isGuest: false,
};

test('createInstantRouteHandlerBody creates a typed sync-user body', () => {
expect(
createInstantRouteHandlerBody('sync-user', {
appId: 'app-id',
user,
}),
).toEqual({
type: 'sync-user',
appId: 'app-id',
user,
});
});

test('createInstantRouteHandler accepts a sync-user body from the shared helper', async () => {
const handler = createInstantRouteHandler({ appId: 'app-id' });
const response = await handler.POST(
new Request('https://example.com/api/instant', {
method: 'POST',
body: JSON.stringify(
createInstantRouteHandlerBody('sync-user', {
appId: 'app-id',
user,
}),
),
}),
);

expect(response.status).toBe(200);
expect(await response.json()).toEqual({ ok: true });
expect(response.headers.get('set-cookie')).toContain('instant_user_app-id=');
expect(response.headers.get('set-cookie')).toContain('Max-Age=604800');
});

test('createInstantRouteHandler keeps rejecting unknown route handler types', async () => {
const handler = createInstantRouteHandler({ appId: 'app-id' });
const response = await handler.POST(
new Request('https://example.com/api/instant', {
method: 'POST',
body: JSON.stringify({
type: 'future-type',
appId: 'app-id',
}),
}),
);

expect(response.status).toBe(400);
expect(await response.json()).toEqual({
ok: false,
error: 'Unknown type: future-type',
});
});
12 changes: 7 additions & 5 deletions client/packages/core/src/Reactor.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { validate as validateUUID } from 'uuid';
import { WSConnection, SSEConnection } from './Connection.ts';
import { SyncTable } from './SyncTable.ts';
import { InstantStream } from './Stream.ts';
import { createInstantRouteHandlerBody } from './routeHandlerProtocol.ts';

/** @typedef {import('./utils/log.ts').Logger} Logger */
/** @typedef {import('./Connection.ts').Connection} Connection */
Expand Down Expand Up @@ -2202,11 +2203,12 @@ export default class Reactor {
try {
await fetch(this.config.firstPartyPath + '/', {
method: 'POST',
body: JSON.stringify({
type: 'sync-user',
appId: this.config.appId,
user: user,
}),
body: JSON.stringify(
createInstantRouteHandlerBody('sync-user', {
appId: this.config.appId,
user,
}),
),
headers: {
'Content-Type': 'application/json',
},
Expand Down
50 changes: 50 additions & 0 deletions client/packages/core/src/__types__/routeHandlerProtocolTypeTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createInstantRouteHandlerBody } from '../index.ts';
import type {
InstantRouteHandlerBody,
InstantRouteHandlerPayloadByType,
InstantRouteHandlerRawBody,
InstantRouteHandlerType,
User,
} from '../index.ts';
import type { Equal, Expect, NotAny } from './typeUtils.ts';

const user: User = {
id: 'user-id',
refresh_token: 'refresh-token',
isGuest: false,
};

const syncUserBody = createInstantRouteHandlerBody('sync-user', {
appId: 'app-id',
user,
});

type _routeHandlerProtocolCases = [
Expect<NotAny<typeof syncUserBody>>,
Expect<Equal<typeof syncUserBody, InstantRouteHandlerBody<'sync-user'>>>,
Expect<Equal<InstantRouteHandlerType, 'sync-user'>>,
Expect<
Equal<
InstantRouteHandlerPayloadByType['sync-user'],
{ appId: string; user: User | null }
>
>,
];

const rawBody: InstantRouteHandlerRawBody = {
type: 'future-type',
appId: 'app-id',
user: { anything: true },
extra: true,
};

rawBody;

// @ts-expect-error unknown route handler types should not be constructible.
createInstantRouteHandlerBody('unknown-type', { appId: 'app-id', user });

createInstantRouteHandlerBody('sync-user', {
appId: 'app-id',
// @ts-expect-error sync-user bodies use the official User shape.
user: { refresh_token: 'refresh-token' },
});
8 changes: 6 additions & 2 deletions client/packages/core/src/createRouteHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { User } from './clientTypes.js';
import type { InstantRouteHandlerRawBody } from './routeHandlerProtocol.ts';

type CreateRouteHandlerConfig = {
appId: string;
Expand Down Expand Up @@ -37,7 +38,7 @@ function errorResponse(status: number, message: string) {
export const createInstantRouteHandler = (config: CreateRouteHandlerConfig) => {
return {
POST: async (req: Request) => {
let body: { type?: string; appId?: string; user?: User | null };
let body: InstantRouteHandlerRawBody;
try {
body = await req.json();
} catch {
Expand All @@ -54,7 +55,10 @@ export const createInstantRouteHandler = (config: CreateRouteHandlerConfig) => {

switch (body.type) {
case 'sync-user':
return createUserSyncResponse(config, body.user ?? null);
return createUserSyncResponse(
config,
(body.user as User | null | undefined) ?? null,
);
default:
return errorResponse(400, `Unknown type: ${body.type}`);
}
Expand Down
12 changes: 12 additions & 0 deletions client/packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ import {
type StoreInterfaceStoreName,
} from './utils/PersistedObject.ts';
import { createInstantRouteHandler } from './createRouteHandler.ts';
import {
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
} from './routeHandlerProtocol.ts';
import { parseSchemaFromJSON } from './parseSchemaFromJSON.ts';

import type {
Expand Down Expand Up @@ -1225,6 +1232,11 @@ export {
type StoreInterfaceClass,
type StoreInterfaceStoreName,
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
};

/** @deprecated Use StoreInterface instead */
Expand Down
46 changes: 46 additions & 0 deletions client/packages/core/src/routeHandlerProtocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { User } from './clientTypes.ts';

/**
* Known payloads sent to `firstPartyPath` and handled by
* `createInstantRouteHandler`.
*/
export type InstantRouteHandlerPayloadByType = {
'sync-user': {
appId: string;
user: User | null;
};
};

/** Known `type` values for Instant route handler request bodies. */
export type InstantRouteHandlerType = keyof InstantRouteHandlerPayloadByType;

/** A valid request body for Instant's first-party route handler protocol. */
export type InstantRouteHandlerBody<
Type extends InstantRouteHandlerType = InstantRouteHandlerType,
> = {
[KnownType in Type]: {
type: KnownType;
} & InstantRouteHandlerPayloadByType[KnownType];
}[Type];

/**
* An untrusted request body before route handler validation.
*
* Use `InstantRouteHandlerBody` after checking the `type` and payload shape.
*/
export type InstantRouteHandlerRawBody = {
type?: InstantRouteHandlerType | (string & {});
appId?: string;
user?: unknown;
[key: string]: unknown;
};

/** Creates a typed request body for Instant's first-party route handler. */
export function createInstantRouteHandlerBody<
Type extends InstantRouteHandlerType,
>(
type: Type,
payload: InstantRouteHandlerPayloadByType[Type],
): InstantRouteHandlerBody<Type> {
return { type, ...payload } as InstantRouteHandlerBody<Type>;
}
10 changes: 10 additions & 0 deletions client/packages/react-native/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import {

// types
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
type RoomSchemaShape,
type InstantQuery,
type InstantQueryResult,
Expand Down Expand Up @@ -190,6 +195,11 @@ export {
lookup,
i,
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
InstantReactNativeDatabase,

// error
Expand Down
10 changes: 10 additions & 0 deletions client/packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ import {
type SyncTableSetupError,
StoreInterface,
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
type StoreInterfaceStoreName,
InstantWritableStream,
InstantReadableStream,
Expand Down Expand Up @@ -212,4 +217,9 @@ export {

// Server helper
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
};
9 changes: 8 additions & 1 deletion client/packages/react/src/next-ssr/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ export {
export { InstantNextDatabase } from './InstantNextDatabase.tsx';
export { InstantSuspenseProvider } from './InstantSuspenseProvider.tsx';

export { createInstantRouteHandler } from '@instantdb/core';
export {
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
} from '@instantdb/core';

/**
*
Expand Down
10 changes: 10 additions & 0 deletions client/packages/solidjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ import {
type SyncTableSetupError,
StoreInterface,
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
type StoreInterfaceStoreName,
} from '@instantdb/core';

Expand Down Expand Up @@ -191,4 +196,9 @@ export {

// Server helper
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
};
10 changes: 10 additions & 0 deletions client/packages/svelte/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ import {
type SyncTableSetupError,
StoreInterface,
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
type StoreInterfaceStoreName,
} from '@instantdb/core';

Expand Down Expand Up @@ -197,4 +202,9 @@ export {

// Server helper
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
};
10 changes: 10 additions & 0 deletions client/packages/vue/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ import {
type SyncTableSetupError,
StoreInterface,
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
type StoreInterfaceStoreName,
} from '@instantdb/core';

Expand Down Expand Up @@ -196,4 +201,9 @@ export {

// Server helper
createInstantRouteHandler,
createInstantRouteHandlerBody,
type InstantRouteHandlerBody,
type InstantRouteHandlerPayloadByType,
type InstantRouteHandlerRawBody,
type InstantRouteHandlerType,
};