Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changes/foundation-tasks-and-webhooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@simulacrum/foundation-simulator": minor:feat
---

Add tasks to handle generic functions that would run in an `effection` scope. This enables the ability to handle webhooks. A webhook is an action that can be triggered directly and makes a `POST` to a specified endpoint, but can also watch and trigger on updates to the store.
4 changes: 2 additions & 2 deletions packages/auth0/src/handlers/auth0-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export const createAuth0Handlers = (
},

["/userinfo"]: function (req, res) {
let token = null;
let token: string | null = null;
if (req.headers.authorization) {
let authorizationHeader = req.headers.authorization;
token = authorizationHeader?.split(" ")?.[1];
Expand Down Expand Up @@ -272,7 +272,7 @@ export const createAuth0Handlers = (
logger.log({ "/passwordless/start": { body: req.body } });

try {
const { client_id, connection, email, phone_number, send } = req.body;
const { client_id, connection, email, phone_number } = req.body;

// Validate required fields
if (!client_id) {
Expand Down
13 changes: 9 additions & 4 deletions packages/auth0/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {
type SimulationHandlers,
type FoundationSimulator,
} from "@simulacrum/foundation-simulator";
import { type ExtendedSimulationStore, extendStore } from "./store/index.ts";
import type { ExtendedSimulationStore } from "./store/index.ts";
import { extendStore } from "./store/index.ts";
import type { Router } from "express";
import type { Auth0ExtendStoreInput } from "./store/index.ts";
import { extendRouter } from "./handlers/index.ts";
import {
type Auth0InitialStore,
Expand All @@ -16,16 +19,18 @@ export type Auth0Simulator = (args?: {
debug?: boolean;
initialState?: Auth0InitialStore;
extend?: {
extendStore?: SimulationInput["extendStore"];
extendStore?: Auth0ExtendStoreInput;
openapiHandlers?: (
simulationStore: ExtendedSimulationStore
) => SimulationHandlers;
extendRouter?: SimulationInput["extendRouter"];
extendRouter?: (
router: Router,
simulationStore: ExtendedSimulationStore
) => void;
};
options?: Partial<Auth0Configuration>;
}) => FoundationSimulator<ExtendedSimulationStore>;

type SimulationInput = Parameters<typeof createFoundationSimulationServer>[0];
export const simulation: Auth0Simulator = (args = {}) => {
const config = getConfig(args.options);
const parsedInitialState = !args?.initialState
Expand Down
87 changes: 54 additions & 33 deletions packages/auth0/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import type {
ExtendSimulationSelectorsInput,
TableOutput,
AnyState,
ExtendSimulationActionsInputLoose,
ExtendSimulationSelectorsInputLoose,
ExtendStoreConfig,
} from "@simulacrum/foundation-simulator";
import {
convertInitialStateToStoreState,
Expand All @@ -25,10 +28,14 @@ export type ExtendedSchema = ({ slice }: ExtendSimulationSchema) => {
};
type ExtendActions = typeof inputActions;
type ExtendSelectors = typeof inputSelectors;
export type Auth0Schema = ReturnType<ExtendedSchema>;
export type Auth0Actions = ReturnType<ExtendActions>;
export type Auth0Selectors = ReturnType<ExtendSelectors>;

export type ExtendedSimulationStore = SimulationStore<
ReturnType<ExtendedSchema>,
ReturnType<ExtendActions>,
ReturnType<ExtendSelectors>
Auth0Schema,
Auth0Actions,
Auth0Selectors
>;

const inputSchema =
Expand Down Expand Up @@ -56,49 +63,63 @@ const inputSchema =
return slices;
};

const inputActions = (args: ExtendSimulationActions<ExtendedSchema>) => {
return {};
const inputActions = (_args: ExtendSimulationActions<ExtendedSchema>) => {
return {} as ExtendSimulationActions<ExtendedSchema>;
};

const extendActions =
(extendedActions?: ExtendSimulationActionsInput<any, ExtendedSchema>) =>
(
extendedActions?: ExtendSimulationActionsInputLoose<
Auth0Actions,
Auth0Schema
>
) =>
(args: ExtendSimulationActions<ExtendedSchema>) => {
return extendedActions
? // @ts-expect-error schema is cyclical, ignore extension for now
{ ...inputActions(args), ...extendedActions(args) }
: inputActions(args);
const base = inputActions(args);
if (!extendedActions) return base;
const extResult = extendedActions(args);
return {
...(base as object),
...(extResult as object),
} as Auth0Actions;
};

const inputSelectors = (args: ExtendSimulationSelectors<ExtendedSchema>) => {
const { createSelector, schema } = args;
return {};
const inputSelectors = (_args: ExtendSimulationSelectors<ExtendedSchema>) => {
return {} as ExtendSimulationSelectors<ExtendedSchema>;
};

const extendSelectors =
(extendedSelectors?: ExtendSimulationSelectorsInput<any, ExtendedSchema>) =>
(
extendedSelectors?: ExtendSimulationSelectorsInputLoose<
Auth0Selectors,
Auth0Schema
>
) =>
(args: ExtendSimulationSelectors<ExtendedSchema>) => {
return extendedSelectors
? // @ts-expect-error schema is cyclical, ignore extension for now
{ ...inputSelectors(args), ...extendedSelectors(args) }
: inputSelectors(args);
const base = inputSelectors(args);
if (!extendedSelectors) return base;
const extResult = extendedSelectors(args);
return {
...(base as object),
...(extResult as object),
} as Auth0Selectors;
};

export const extendStore = <T>(
export type Auth0ExtendStoreInput = ExtendStoreConfig<
Auth0Schema,
Auth0Actions,
Auth0Selectors
>;

export const extendStore = (
initialState: Auth0InitialStore | undefined,
extended:
| {
actions: ExtendSimulationActionsInput<
any,
ExtendSimulationSchemaInput<T>
>;
selectors: ExtendSimulationSelectorsInput<
any,
ExtendSimulationSchemaInput<T>
>;
schema?: ExtendSimulationSchemaInput<T>;
}
| undefined
) => ({
extended?: Auth0ExtendStoreInput
): {
schema: ExtendSimulationSchemaInput<Auth0Schema>;
actions?: ExtendSimulationActionsInput<Auth0Actions, Auth0Schema>;
selectors?: ExtendSimulationSelectorsInput<Auth0Selectors, Auth0Schema>;
logs?: boolean;
} => ({
actions: extendActions(extended?.actions),
selectors: extendSelectors(extended?.selectors),
schema: inputSchema(initialState, extended?.schema),
Expand Down
4 changes: 2 additions & 2 deletions packages/auth0/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export const frontendSimulation: ReturnType<
> = createFoundationSimulationServer({
port: 3000,
// dummy route so it returns a 200 at `/`
extendRouter(router, simulationStore) {
router.get("/", (req, res) => {
extendRouter(router, _simulationStore) {
router.get("/", (_req, res) => {
res.sendStatus(200);
});
},
Expand Down
2 changes: 1 addition & 1 deletion packages/auth0/test/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe("rules", () => {
let code: string;
let server: FoundationSimulatorListening<unknown>;

beforeEach(async ({ task }) => {
beforeEach(async ({ task: _task }) => {
({ code, server } = await createSimulation(
"test/fixtures/rules-access-token"
));
Expand Down
2 changes: 1 addition & 1 deletion packages/foundation/example/extensiveServer/extend-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const extendRouter = (
router: Router,
simulationStore: ExtendedSimulationStore
) => {
router.get("/extended-route", (req, res) => {
router.get("/extended-route", (_req, res) => {
let dogs = simulationStore.schema.dogs.select(
simulationStore.store.getState()
);
Expand Down
21 changes: 19 additions & 2 deletions packages/foundation/example/extensiveServer/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ const openapiSchemaFromRealEndpoint = {
},
},
},
"/trigger-webhook": {
post: {
summary: "Trigger a webhook",
operationId: "triggerWebhook",
responses: {
200: {
description: "webhook sent",
},
},
},
},
},
};

Expand Down Expand Up @@ -130,7 +141,7 @@ function handlers(
simulationStore: ExtendedSimulationStore
): SimulationHandlers {
return {
getDogs: (_c, request, response, _next, routeMetadata) => {
getDogs: (_c, _request, response, _next, routeMetadata) => {
let dogs = simulationStore.schema.dogs.select(
simulationStore.store.getState()
);
Expand All @@ -140,7 +151,7 @@ function handlers(
response.sendStatus(routeMetadata.defaultCode);
}
},
putDogs: (c, req, response) => {
putDogs: (_c, _req, response) => {
simulationStore.store.dispatch(
simulationStore.actions.batchUpdater([
simulationStore.schema.dogs.increment(),
Expand Down Expand Up @@ -169,6 +180,12 @@ function handlers(
);
response.status(200).json({ dogs });
},
triggerWebhook: (c, _request, response) => {
simulationStore.store.dispatch(
simulationStore.actions.webhooks.onTest(c.request.body)
);
response.status(200).json({ status: "ok" });
},
};
}

Expand Down
56 changes: 45 additions & 11 deletions packages/foundation/example/extensiveServer/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,35 @@ import type {
SimulationStore,
ExtendSimulationActions,
ExtendSimulationSelectors,
ExtendSimulationTasks,
ExtendSimulationSchema,
AnyState,
ExtendStoreConfig,
} from "../../src/index.ts";

export type ExtendedSchema = typeof inputSchema;
type ExtendActions = typeof inputActions;
type ExtendSelectors = typeof inputSelectors;
export type ExtendedSchema = typeof schema;
export type ExtendActions = typeof actions;
export type ExtendSelectors = typeof selectors;
// `tasks` is a function that returns { tasks: Callable[]; actions: Actions }
// Export the Actions portion as the `ExtendTasks` type so it can be used
// as the fourth generic parameter to `SimulationStore` (which expects the
// actions shape, not the whole return type of the tasks function).
export type ExtendTasks = ReturnType<typeof tasks>["actions"];
export type ExtendedSimulationStore = SimulationStore<
ReturnType<ExtendedSchema>,
ReturnType<ExtendActions>,
ReturnType<ExtendSelectors>
ReturnType<ExtendSelectors>,
ExtendTasks
>;

const inputSchema = ({ slice }: ExtendSimulationSchema) => {
const schema = ({ slice }: ExtendSimulationSchema) => {
let slices = {
dogs: slice.num(),
};
return slices;
};

const inputActions = ({
const actions = ({
thunks,
schema,
}: ExtendSimulationActions<ExtendedSchema>) => {
Expand All @@ -38,7 +46,7 @@ const inputActions = ({
return { addLotsOfDogs };
};

const inputSelectors = ({
const selectors = ({
createSelector,
schema,
}: ExtendSimulationSelectors<ExtendedSchema>) => {
Expand All @@ -53,9 +61,35 @@ const inputSelectors = ({
return { booleanSpecificNumbers };
};

export const extendStore = {
const tasks = ({ createWebhook }: ExtendSimulationTasks<ExtendedSchema>) => {
const webhook = createWebhook("https://example.com/webhook");
const onTest = webhook.create<{ id: string; name: string }>(
"webhook:test",
function* (ctx, next) {
// the following would send off that request
// but we don't want to post in tests
ctx.request = ctx.req({
body: JSON.stringify(ctx.payload),
});

// calling this will proceed through the middleware chain
// and actually send the request
yield* next();
}
);

return { tasks: [webhook.task], actions: { webhooks: { onTest } } };
};

export const extendStore: ExtendStoreConfig<
ReturnType<ExtendedSchema>,
ReturnType<ExtendActions>,
ReturnType<ExtendSelectors>,
ReturnType<typeof tasks>["actions"]
> = {
logs: false,
actions: inputActions,
selectors: inputSelectors,
schema: inputSchema,
actions,
selectors,
tasks,
schema,
};
11 changes: 3 additions & 8 deletions packages/foundation/example/singleFileServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,12 @@ export function simulation(): FoundationSimulator<any> {
],
handlers({ store, schema, actions }) {
return {
getDogs: (c, req, res) => {
getDogs: (_c, _req, res) => {
let dogs = schema.boop.select(store.getState());
res.status(200).json({ dogs });
},
putDogs: (c, req, res) => {
putDogs: (_c, _req, res) => {
store.dispatch(actions.batchUpdater([schema.boop.increment()]));
// TODO the looped around TS does not seem to tackle this well
// store.dispatch(actions.upsertTest({ ["1"]: { name: "Friend" } }));
res.sendStatus(200);
},
};
Expand All @@ -100,7 +98,6 @@ export function simulation(): FoundationSimulator<any> {
extendStore: {
logs: false,
actions: ({ thunks, schema }) => {
// TODO attempt to remove this type as a requirement
let upsertTest = thunks.create<AnyState>(
"user:upsert",
function* boop(ctx, next) {
Expand All @@ -114,9 +111,7 @@ export function simulation(): FoundationSimulator<any> {

return { upsertTest };
},
selectors: () => ({}),
schema: ({ slice }: ExtendSimulationSchema) => {
// TODO attempt to remove this type as a requirement
let slices = {
test: slice.table(),
booping: slice.str(),
Expand All @@ -126,7 +121,7 @@ export function simulation(): FoundationSimulator<any> {
},
},
extendRouter(router, simulationStore) {
router.get("/extended-route", (req, res) => {
router.get("/extended-route", (_req, res) => {
let dogs = simulationStore.schema.boop.select(
simulationStore.store.getState()
);
Expand Down
Loading
Loading