@simulacrum/github-api-simulator
Version:
Provides common functionality to frontend app and plugins.
235 lines (222 loc) • 6.87 kB
text/typescript
import type {
SimulationStore,
ExtendSimulationSchema,
ExtendSimulationSchemaInput,
ExtendSimulationActions,
ExtendSimulationActionsInput,
ExtendSimulationSelectors,
ExtendSimulationSelectorsInput,
TableOutput,
AnyState,
} from "@simulacrum/foundation-simulator";
import {
convertInitialStateToStoreState,
type GitHubStore,
type GitHubBlob,
type GitHubOrganization,
type GitHubRepository,
type GitHubUser,
type GitHubBranch,
GitHubAppInstallation,
} from "./entities.ts";
export type ExtendedSchema = ({ slice }: ExtendSimulationSchema) => {
users: (
n: string
) => TableOutput<GitHubUser, AnyState, GitHubUser | undefined>;
installations: (
n: string
) => TableOutput<
GitHubAppInstallation,
AnyState,
GitHubAppInstallation | undefined
>;
repositories: (
n: string
) => TableOutput<GitHubRepository, AnyState, GitHubRepository | undefined>;
branches: (
n: string
) => TableOutput<GitHubBranch, AnyState, GitHubBranch | undefined>;
organizations: (
n: string
) => TableOutput<
GitHubOrganization,
AnyState,
GitHubOrganization | undefined
>;
blobs: (
n: string
) => TableOutput<GitHubBlob, AnyState, GitHubBlob | undefined>;
};
type ExtendActions = typeof inputActions;
type ExtendSelectors = typeof inputSelectors;
export type ExtendedSimulationStore = SimulationStore<
ReturnType<ExtendedSchema>,
ReturnType<ExtendActions>,
ReturnType<ExtendSelectors>
>;
const inputSchema =
<T>(
initialState?: GitHubStore,
extendedSchema?: ExtendSimulationSchemaInput<T>
) =>
({ slice }: ExtendSimulationSchema) => {
const storeInitialState = convertInitialStateToStoreState(initialState);
const extended = extendedSchema ? extendedSchema({ slice }) : {};
let slices = {
users: slice.table<GitHubUser>(
!storeInitialState ? {} : { initialState: storeInitialState.users }
),
installations: slice.table<GitHubAppInstallation>(
!storeInitialState
? {}
: { initialState: storeInitialState.installations }
),
repositories: slice.table<GitHubRepository>(
!storeInitialState
? {}
: { initialState: storeInitialState.repositories }
),
branches: slice.table<GitHubBranch>(
!storeInitialState ? {} : { initialState: storeInitialState.branches }
),
organizations: slice.table<GitHubOrganization>(
!storeInitialState
? {}
: { initialState: storeInitialState.organizations }
),
blobs: slice.table<GitHubBlob>(
!storeInitialState ? {} : { initialState: storeInitialState.blobs }
),
...extended,
};
return slices;
};
const inputActions = (args: ExtendSimulationActions<ExtendedSchema>) => {
return {};
};
const extendActions =
(extendedActions?: ExtendSimulationActionsInput<any, ExtendedSchema>) =>
(args: ExtendSimulationActions<ExtendedSchema>) => {
return extendedActions
? // @ts-expect-error schema is cyclical, ignore extension for now
{ ...inputActions(args), ...extendedActions(args) }
: inputActions(args);
};
const inputSelectors = (args: ExtendSimulationSelectors<ExtendedSchema>) => {
const { createSelector, schema } = args;
const allGithubOrganizations = createSelector(
schema.organizations.selectTableAsList,
(ghOrgs) => {
return ghOrgs;
}
);
const getAppInstallation = createSelector(
schema.installations.selectTableAsList,
schema.organizations.selectTableAsList,
schema.repositories.selectTableAsList,
(_: AnyState, org: string, repo?: string) => ({ org, repo }),
(installations, orgs, repos, { org, repo }) => {
const appInstall = installations.find(
(install) => install.account === org
);
if (!appInstall) return undefined;
let account = undefined;
if (repo) {
const repoData = repos.find(
(r) => r.owner === appInstall?.account && r.name === repo
);
if (repoData) account = orgs.find((o) => o.login === repoData.owner);
} else {
account = orgs.find((o) => o.login === appInstall?.account);
}
if (!account) return undefined;
return {
...appInstall,
account: { ...account },
target_id: account?.id,
target_type: account?.type,
};
}
);
const allReposWithOrgs = createSelector(
schema.repositories.selectTableAsList,
schema.organizations.selectTable,
(_: AnyState, org?: string) => org,
(allRepos, orgMap, org) => {
if (org && !orgMap?.[org]) return undefined;
const repos = !org ? allRepos : allRepos.filter((r) => r.owner === org);
return repos.map((repo) => {
const linkedRepo = { ...repo, owner: { ...orgMap[repo.owner] } };
// TODO better option than delete?
delete linkedRepo.owner.name;
delete linkedRepo.owner.email;
return linkedRepo;
});
}
);
const getBlob = createSelector(
schema.blobs.selectTableAsList,
(_state: AnyState, owner: string, repo: string, sha_or_path: string) => ({
owner,
repo,
sha_or_path,
}),
(blobs, { owner, repo, sha_or_path }) => {
const blob = blobs.find(
(blob) =>
blob.owner === owner &&
blob.repo === repo &&
(blob.path === sha_or_path || blob.sha === sha_or_path)
);
return blob;
}
);
const getBlobAtOwnerRepo = createSelector(
schema.blobs.selectTableAsList,
(_state: AnyState, owner: string, repo: string) => ({
owner,
repo,
}),
(blobs, { owner, repo }) => {
const blob = blobs.filter(
(blob) => blob.owner === owner && blob.repo === repo
);
return blob;
}
);
return {
allGithubOrganizations,
getAppInstallation,
allReposWithOrgs,
getBlob,
getBlobAtOwnerRepo,
};
};
const extendSelectors =
(extendedSelectors?: ExtendSimulationSelectorsInput<any, ExtendedSchema>) =>
(args: ExtendSimulationSelectors<ExtendedSchema>) => {
return extendedSelectors
? // @ts-expect-error schema is cyclical, ignore extension for now
{ ...inputSelectors(args), ...extendedSelectors(args) }
: inputSelectors(args);
};
export const extendStore = <T>(
initialState: GitHubStore | undefined,
extended:
| {
actions: ExtendSimulationActionsInput<
any,
ExtendSimulationSchemaInput<T>
>;
selectors: ExtendSimulationSelectorsInput<
any,
ExtendSimulationSchemaInput<T>
>;
schema?: ExtendSimulationSchemaInput<T>;
}
| undefined
) => ({
actions: extendActions(extended?.actions),
selectors: extendSelectors(extended?.selectors),
schema: inputSchema(initialState, extended?.schema),
});