-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: type duplication bug #938
fix: type duplication bug #938
Conversation
...snapshots/typegraph_core__runtimes__prisma__type_generation__test__update_many Post inp.snap
Show resolved
Hide resolved
typegraphimport { fx, Policy, t, typegraph } from "@typegraph/sdk";
import { Auth } from "@typegraph/sdk/params";
import { KvRuntime } from "@typegraph/sdk/runtimes/kv";
import { DenoRuntime } from "@typegraph/sdk/runtimes/deno";
import { PrismaRuntime } from "@typegraph/sdk/providers/prisma";
import { S3Runtime } from "@typegraph/sdk/providers/aws";
import { dbg, rack, rootBuilder } from "./utils.js";
import { TypegraphBuilderArgs } from "@typegraph/sdk/typegraph";
import {
Backend,
SubstantialRuntime,
WorkflowFile,
} from "@typegraph/sdk/runtimes/substantial";
const timestamp = () => t.integer();
export default function vivavox(g: TypegraphBuilderArgs) {
using root = rootBuilder(g);
const basicUsers = {
// key used by the web app
vivavox_web: "vivavox_web",
worker: "worker",
};
const substantialCommon = [
"funcs/types.ts",
"funcs/client.ts",
"funcs/utils.ts",
];
const substantial_files = [
WorkflowFile.deno("funcs/response.ts", [...substantialCommon])
.import(["responseSession"])
.build(),
WorkflowFile.deno("funcs/ingress.ts", [...substantialCommon])
.import(["startIngress"])
.build(),
];
const rts = {
deno: new DenoRuntime(),
kvCache: new KvRuntime("CACHE_REDIS_URL"),
prisma: new PrismaRuntime("vivavox", "POSTGRES_CONN"),
sub: new SubstantialRuntime(Backend.devMemory(), substantial_files),
s3: new S3Runtime({
hostSecret: "VIVAVOX_S3_HOST",
regionSecret: "VIVAVOX_S3_REGION",
accessKeySecret: "VIVAVOX_S3_ACCESS_KEY",
secretKeySecret: "VIVAVOX_S3_SECRET_KEY",
pathStyleSecret: "VIVAVOX_S3_PATH_STYLE",
}),
};
g.auth(Auth.basic([basicUsers.vivavox_web, basicUsers.worker]));
g.auth(Auth.oauth2Google("profile email openid"));
const pol = {
pub: Policy.public(),
internal: Policy.internal(),
vivavoxWebAllowAll: rts.deno.policy(
"vivavoxWebAllowAll",
`(_args, { context }) => context.username == "${basicUsers.vivavox_web}" || null`,
),
workerAllowAll: rts.deno.policy(
"workerAllowAll",
`(_args, { context }) => context.username == "${basicUsers.worker}" || null`,
),
};
const commonFields = {
// FIXME: injected fields are still required by prisma
// create functions
// FIXME: applying simple (non-PerEffect) injection eliminates field from Model
// FIXME: PerEffect injections run into weird permission errors like update being
// denied for createdAt on a create query
// FIXME: datetime doesn't expose a orderBy clause in prisma
createdAt: t.datetime(),
updatedAt: t.datetime(),
// TODO: JSONB table mirror for entity tables to collect delted items
// this should be easily a ts function
};
const userRoles = t.enum_(["admin", "manager", "reviewer"]);
// the main entities of vivavox, each
// represented by a db table
const tDomain = rack({
webSession: t.struct({
// TODO: MET-764 uuidv7??
id: t.uuid({ asId: true, config: { auto: true } }),
// TODO: use numbers for efficency??
ipAddr: t.string(),
userAgent: t.string(),
// user_id: t.string().optional(), // TODO: connect with user on login
// TODO: connect with oauth2, etc sessions
expiresAt: t.datetime(),
...commonFields,
}),
// FIXME: could it be possible to type entt
// so that we can make sure `g.ref` gets valid names?
scenario: (/* entt */) =>
t.struct({
id: t.uuid({ config: ["auto"] }).id(),
title: t.string(),
body: t.string(),
publishedAt: t.datetime().optional(),
scenes: t.list(g.ref("scene")),
links: t.list(g.ref("scenarioLink")),
author: g.ref("user"),
...commonFields,
}),
scenarioLink: t.struct({
id: t.uuid({ config: { auto: true } }).id(),
// TODO: unique index on slug to accelerate lookup
// we're doing tablescans today
slug: t.string(),
slugRecipe: t.json(),
closedAt: timestamp().optional(),
scenario: g.ref("scenario"),
attachedSessions: t.list(g.ref("vivaSession")),
...commonFields,
}),
// use different counters for different
// needs. By default, just go for the counter
// under the key `default`.
// Only make a separate counter if you need to generate
// a lot of items for a specific usecase and
sqidCounters: t.struct({
key: t.string().id(),
number: t.integer({ defaultValue: 0 }),
}),
scene: t.struct(
{
id: t.uuid({ asId: true, config: { auto: true } }),
title: t.string(),
description: t.string(),
order: t.integer(),
video: g.ref("sceneVideo").optional(),
scenario: g.ref("scenario"),
// FIXME: MET-762 this leads to typegraph too large
// to even serialize
// responseVideo: t.list(g.ref("responseVideo")),
},
{ config: { unique: ["scenario", "order"] } },
),
sceneVideo: t.struct({
id: t.uuid({ asId: true, config: { auto: true } }),
scene: g.ref("scene"),
duration: t.float(),
type: t.string(), // mime type
width: t.integer(),
height: t.integer(),
recordingStartedAt: t.datetime(),
// FIXME: consider storing the object path here
// directly
// filePath: t.string(),
...commonFields,
}),
vivaSession: t.struct({
id: t.uuid({ config: { auto: true } }).id(),
email: t.string({ format: "email" }),
sourceScenarioLink: g.ref("scenarioLink"),
specialLinkSlug: t.string(),
response: g.ref("response").optional(),
// UTC unix timestamp
// to get access to comparision
// operators in prisma
expiresAtTs: timestamp(),
...commonFields,
}),
response: t.struct({
id: t.uuid({ config: { auto: true } }).id(),
session: g.ref("vivaSession"),
hiddenAt: t.datetime().optional(),
videos: t.list(g.ref("responseVideo")),
...commonFields,
}),
responseVideo: t.struct({
id: t.uuid({ config: { auto: true } }).id(),
response: g.ref("response"),
// FIXME: MET-762
// scene: g.ref("scene"),
sceneIdPlaceholder: t.string(),
filePath: t.string(),
recordingStartedAt: t.datetime(),
...commonFields,
}),
user: t.struct(
{
id: t.uuid({ config: { auto: true } }).id(),
name: t.string(),
email: t.email().optional(),
providerName: t.string(),
providerId: t.string(),
organizations: t.list(g.ref("userOrganization")),
scenarios: t.list(g.ref("scenario")),
// picture: t.uri().optional()
},
{ config: { unique: [["providerName", "providerId"]] } },
),
organization: t.struct({
id: t.uuid({ config: { auto: true } }).id(),
name: t.string(),
email: t.email(),
users: t.list(g.ref("userOrganization")),
}),
userOrganization: t.struct({
id: t.integer({}, { config: { auto: true } }).id(),
user: g.ref("user"),
organization: g.ref("organization"),
role: userRoles,
}),
invitation: t.struct({
id: t.integer({}, { config: { auto: true } }).id(),
recipient: t.email(),
organization: g.ref("organization"),
role: userRoles,
}),
//orgPreferences: t.struct({
// id: t.uuid({ config: { auto: true } }).id(),
// domain: t.string(),
// organization: g.ref("organization"),
//}),
});
// utility shared types
// must not be used in prisma
const tUtil = rack({
sceneMetadata: t.struct({
title: t.string(),
description: t.string(),
}),
});
// we use a seed custom functions so that we don't
// have to expose the prisma functions (they're internal)
root.expose(
{
seedTest: rts.deno
.import(t.struct({}), t.boolean(), {
effect: fx.create(true),
name: "seedTest",
module: "./funcs/seeds.ts",
deps: ["./funcs/fdk.ts", "./funcs/client.ts", "./funcs/utils.ts"],
})
.rename("seedTest")
.withPolicy(pol.pub),
createScenariosInternal: rts.prisma.createMany(tDomain.scenario),
},
pol.internal,
);
// endpoints for web sessions
root.expose(
{
createWebSession: rts.prisma.create(tDomain.webSession),
findWebSession: rts.prisma.findFirst(tDomain.webSession),
webSessionCacheGet: rts.kvCache.get(),
webSessionCacheSet: rts.kvCache.set(),
webSessionCacheDel: rts.kvCache.delete(),
},
[pol.vivavoxWebAllowAll, pol.internal],
);
// endpoints for viva sessions
root.expose(
{
findVivaSession: rts.prisma.findFirst(tDomain.vivaSession),
createVivaSessionInternal: rts.prisma
.create(tDomain.vivaSession)
.withPolicy(pol.internal),
updateVivaSessionInternal: rts.prisma
.update(tDomain.vivaSession)
.withPolicy(pol.internal),
emailVivaSessionLink: rts.deno
.import(
t.struct({
scenarioId: t.string(),
address: t.email(),
linkSlug: t.string(),
}),
t.boolean(),
{
effect: fx.create(false),
name: "sendEmailLink",
module: "./funcs/session.ts",
deps: ["./funcs/fdk.ts", "./funcs/client.ts", "./funcs/utils.ts"],
secrets: [
"VIVA_SESSION_LIFESPAN_SECS",
"VIVAVOX_WEB_URL",
"MAIL_SERVICE_URL",
"MAILIT_CREDS",
"MAIL_SENDER_ADDR",
"MAIL_SENDER_NAME",
"MAIL_SUPPORT_ADDR",
],
},
)
.rename("emailVivaSessionLink"),
},
[pol.vivavoxWebAllowAll, pol.internal],
);
// endpoints for responses
root.expose(
{
findResponse: rts.prisma.findFirst(tDomain.response),
findResponses: rts.prisma.findMany(tDomain.response),
hideResponse: rts.prisma.update(tDomain.response).reduce({
where: g.inherit(),
data: {
hiddenAt: g.inherit(),
},
}),
},
pol.pub,
);
root.expose(
{
createSceneInternal: rts.prisma.create(tDomain.scene),
aggregateScenesInternal: rts.prisma.aggregate(tDomain.scene).apply({
where: { scenario: { id: g.asArg("scenarioId") } },
}),
// setSceneOrderInternal: rts.prisma.update(tDomain.scene)
// .apply({
// where: { id: g.asArg("sceneId") },
// data: { order: g.asArg("order") }
// }),
},
pol.internal,
);
root.expose(
{
publishScenario: rts.deno.import(
t.struct({
scenarioId: t.string(),
}),
t.struct({
scenarioId: t.string(),
slug: t.string(),
/* scenario: rts.prisma.findFirst(tDomain.scenario).reduce({
where: {
id: g.inherit().fromParent("scenarioId"),
},
}), */
}),
{
effect: fx.update(true),
name: "publishScenario",
module: "./funcs/scenario.ts",
deps: ["./funcs/fdk.ts", "./funcs/client.ts", "./funcs/utils.ts"],
},
),
updateShortLinkInternal: rts.prisma
.update(tDomain.scenarioLink)
.withPolicy(pol.internal),
createScenario: rts.prisma.create(tDomain.scenario),
deleteScenario: rts.prisma.delete(tDomain.scenario),
updateScenario: rts.prisma.update(tDomain.scenario),
findAllScenarios: rts.prisma.findMany(tDomain.scenario),
// createScene: rts.prisma.create(tDomain.scene),
findAllScenes: rts.prisma.findMany(tDomain.scene),
// deleteScenario: rts.prisma.delete(tDomain.scenario).apply({
// where: { id: g.asArg("scenarioId") },
// }),
createScene: rts.deno
.import(
t.struct({
scenarioId: t.uuid(),
scene: tUtil.sceneMetadata,
}),
t.uuid(),
{
effect: fx.create(false),
name: "createScene",
module: "./funcs/scene.ts",
deps: ["./funcs/fdk.ts", "./funcs/client.ts"],
},
)
.rename("createScene"),
updateScene: rts.prisma.update(tDomain.scene),
deleteScene: rts.prisma.delete(tDomain.scene),
createUser: rts.prisma.create(tDomain.user),
findUser: rts.prisma.findFirst(tDomain.user),
// updateScene: rts.deno
// .import(tDomain.scene_metadata, t.uuid(), {
// effect: fx.create(false),
// name: "updateScene",
// module: "./funcs/scene.ts",
// deps: ["./funcs/mdk.ts", "./funcs/client.ts"],
// }),
// setSceneOrder: rts.deno
// .import(t.struct({ sceneId: t.uuid(), order: t.integer() }), t.uuid(), {
// effect: fx.create(false),
// name: "setSceneOrder",
// module: "./funcs/scene.ts",
// deps: ["./funcs/mdk.ts", "./funcs/client.ts"],
// }),
},
// FIXME authenticated user?
pol.pub,
);
root.expose(
{
createOrganization: rts.prisma.create(tDomain.organization),
findOrganization: rts.prisma.findFirst(tDomain.organization),
findManyOrganization: rts.prisma.findMany(tDomain.organization),
addUserToOrganization: rts.prisma.create(tDomain.userOrganization),
removeUserFromOrganization: rts.prisma.delete(tDomain.userOrganization),
},
pol.pub, // FIXME
);
// endpoints for scenarios
root.expose(
{
findScenario: rts.prisma.findFirst(tDomain.scenario),
findScene: rts.prisma.findFirst(tDomain.scene),
createVideo: rts.prisma.create(tDomain.sceneVideo),
deleteVideo: rts.prisma.delete(tDomain.sceneVideo),
// endpoint for video files
signUploadUrl: rts.s3.presignPut({ bucket: "vivavox" }),
getDownloadUrl: rts.s3.presignGet({
bucket: "vivavox",
expirySecs: 60 * 60,
}),
},
// FIXME authenticated user?
[pol.vivavoxWebAllowAll, pol.internal],
);
root.expose(
{
startIngress: rts.sub
.start(
t.struct({
webSessionId: t.string(),
roomName: t.string(),
fileName: t.string(),
}),
{
secrets: [
"LIVEKIT_HOST",
"LIVEKIT_KEY",
"LIVEKIT_SECRET",
"WORKER_REDIS_URL",
],
},
)
.reduce({
name: "startIngress",
}),
},
[pol.vivavoxWebAllowAll],
);
// endpoints for responses
root.expose(
{
createResponseInternal: rts.prisma
.create(tDomain.response)
.withPolicy(pol.internal),
createResponse: rts.prisma.create(tDomain.response),
startResponseSession: rts.sub
.start(
t.struct({
sessionId: t.string(),
timeoutSec: t.integer().optional(),
}),
)
.reduce({
name: "responseSession",
}),
sendDevice: rts.sub
.send(
t.struct({
audio: t.boolean(),
video: t.boolean(),
issues: t.string({}, { config: { format: "json" } }).optional(),
}),
)
.reduce({
event: { name: "device" },
}),
sendAnswer: rts.sub
.send(
t.struct({
sceneId: t.string(),
filePath: t.string(),
recordingStartedAt: t.datetime(),
}),
)
.reduce({
event: { name: "answer" },
}),
submitResponse: rts.sub.send(t.boolean()).reduce({
event: { name: "submitResponse" },
}),
results: rts.sub.queryResultsRaw().reduce({ name: "responseSession" }),
abortRun: rts.sub.stop(),
},
pol.vivavoxWebAllowAll,
);
root.expose(
{
getSqidNumber: rts.prisma.upsert(tDomain.sqidCounters).apply({
where: {
// TODO: default values for asArg
key: g.asArg("key"),
},
create: {
// FIXME: allow injecting multiplle leaf apply leaf nodes
// from the same arg
key: g.asArg("key_again"),
number: g.set(0),
},
update: {},
}),
},
[pol.internal],
);
}
const isMainModule = import.meta.url.endsWith(process.argv[1]);
// const isMainModule = import.meta.url === Deno.mainModule;
if (isMainModule) {
await typegraph(
{
name: "vivavox",
cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] },
},
vivavox,
);
}
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #938 +/- ##
==========================================
- Coverage 77.81% 77.76% -0.06%
==========================================
Files 153 153
Lines 18960 18974 +14
Branches 1894 1894
==========================================
+ Hits 14754 14755 +1
- Misses 4182 4195 +13
Partials 24 24 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments...
Migration notes