Initial backend service

This commit is contained in:
Ruslan Bakiev
2026-05-05 12:01:29 +07:00
commit 2357897530
44 changed files with 14773 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
DATABASE_URL=postgresql://mapflow:mapflow@localhost:5432/mapflow
HATCHET_CLIENT_TOKEN=
HOST=0.0.0.0
PORT=4000
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
.env
.DS_Store
+3
View File
@@ -0,0 +1,3 @@
[submodule "instructions"]
path = instructions
url = https://gitea.dsrptlab.com/dsrptlab/instructions
Symlink
+1
View File
@@ -0,0 +1 @@
instructions/instructions.md
+6
View File
@@ -0,0 +1,6 @@
# MapFlow Backend
Fastify + Mercurius GraphQL API for MapFlow.
The backend accepts a user's voice experience, stores a durable record, and
enqueues the Hatchet workflow that will transcribe and analyze it.
Symlink
+1
View File
@@ -0,0 +1 @@
instructions/instructions.md
@@ -0,0 +1,14 @@
mutation CreateVoiceExperience($input: CreateVoiceExperienceInput!) {
createVoiceExperience(input: $input) {
id
status
durationSeconds
place {
id
googlePlaceId
name
latitude
longitude
}
}
}
Submodule
+1
Submodule instructions added at 1a85fbbea6
+1
View File
@@ -0,0 +1 @@
instructions/instructions.md
+5474
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"name": "mapflow-backend",
"version": "1.0.0",
"description": "Fastify + Mercurius GraphQL backend for MapFlow.",
"type": "module",
"main": "dist/server.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"check": "tsc --noEmit",
"dev": "node --import tsx ./src/server.ts",
"start": "node dist/server.js",
"prisma:generate": "prisma generate",
"prisma:migrate:deploy": "prisma migrate deploy"
},
"repository": {
"type": "git",
"url": "git@gitea.dsrptlab.com:mapflow/backend.git"
},
"private": true,
"license": "UNLICENSED",
"dependencies": {
"@hatchet-dev/typescript-sdk": "^1.22.0",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"dotenv": "^17.4.2",
"fastify": "^5.8.5",
"graphql": "^16.13.2",
"mercurius": "^16.9.0",
"pg": "^8.20.0"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@types/pg": "^8.20.0",
"prisma": "^7.8.0",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
}
}
+9
View File
@@ -0,0 +1,9 @@
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL'),
},
});
+41
View File
@@ -0,0 +1,41 @@
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
enum VoiceExperienceStatus {
UPLOADED
TRANSCRIBING
TRANSCRIBED
ANALYZING
ANALYZED
FAILED
}
model Place {
id String @id @default(cuid())
googlePlaceId String @unique
name String
latitude Float
longitude Float
experiences VoiceExperience[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VoiceExperience {
id String @id @default(cuid())
placeId String
place Place @relation(fields: [placeId], references: [id])
durationSeconds Int
audioObjectKey String
status VoiceExperienceStatus @default(UPLOADED)
transcript String?
analysis Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+8
View File
@@ -0,0 +1,8 @@
import 'dotenv/config';
export const config = {
host: process.env.HOST ?? '0.0.0.0',
port: Number(process.env.PORT ?? '4000'),
databaseUrl: process.env.DATABASE_URL ?? '',
hatchetToken: process.env.HATCHET_CLIENT_TOKEN ?? '',
};
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+5
View File
@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+5
View File
@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }
+1
View File
@@ -0,0 +1 @@
export * from "./default"
File diff suppressed because one or more lines are too long
+213
View File
@@ -0,0 +1,213 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
DbNull,
JsonNull,
AnyNull,
NullTypes,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 7.8.0
* Query Engine version: 3c6e192761c0362d496ed980de936e2f3cebcd3a
*/
Prisma.prismaVersion = {
client: "7.8.0",
engine: "3c6e192761c0362d496ed980de936e2f3cebcd3a"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = DbNull
Prisma.JsonNull = JsonNull
Prisma.AnyNull = AnyNull
Prisma.NullTypes = NullTypes
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.PlaceScalarFieldEnum = {
id: 'id',
googlePlaceId: 'googlePlaceId',
name: 'name',
latitude: 'latitude',
longitude: 'longitude',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.VoiceExperienceScalarFieldEnum = {
id: 'id',
placeId: 'placeId',
durationSeconds: 'durationSeconds',
audioObjectKey: 'audioObjectKey',
status: 'status',
transcript: 'transcript',
analysis: 'analysis',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.NullableJsonNullValueInput = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.JsonNullValueFilter = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull,
AnyNull: Prisma.AnyNull
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.VoiceExperienceStatus = exports.$Enums.VoiceExperienceStatus = {
UPLOADED: 'UPLOADED',
TRANSCRIBING: 'TRANSCRIBING',
TRANSCRIBED: 'TRANSCRIBED',
ANALYZING: 'ANALYZING',
ANALYZED: 'ANALYZED',
FAILED: 'FAILED'
};
exports.Prisma.ModelName = {
Place: 'Place',
VoiceExperience: 'VoiceExperience'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
+4541
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+144
View File
@@ -0,0 +1,144 @@
{
"name": "prisma-client-5316d8e66293a4954be6e26169692366997bf25fe9186e98c87e62eed3dc691e",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
"exports": {
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
"import": "./runtime/index-browser.mjs",
"default": "./runtime/index-browser.mjs"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "7.8.0",
"sideEffects": false,
"dependencies": {
"@prisma/client-runtime-utils": "7.8.0"
},
"imports": {
"#wasm-compiler-loader": {
"edge-light": "./wasm-edge-light-loader.mjs",
"workerd": "./wasm-worker-loader.mjs",
"worker": "./wasm-worker-loader.mjs",
"default": "./wasm-worker-loader.mjs"
},
"#main-entry-point": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
}
}
}
@@ -0,0 +1,2 @@
"use strict";var h=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var D=(e,t)=>{for(var n in t)h(e,n,{get:t[n],enumerable:!0})},O=(e,t,n,_)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of M(t))!j.call(e,r)&&r!==n&&h(e,r,{get:()=>t[r],enumerable:!(_=T(t,r))||_.enumerable});return e};var B=e=>O(h({},"__esModule",{value:!0}),e);var xe={};D(xe,{QueryCompiler:()=>F,__wbg_Error_e83987f665cf5504:()=>q,__wbg_Number_bb48ca12f395cd08:()=>C,__wbg_String_8f0eb39a4a4c2f66:()=>k,__wbg___wbindgen_boolean_get_6d5a1ee65bab5f68:()=>W,__wbg___wbindgen_debug_string_df47ffb5e35e6763:()=>V,__wbg___wbindgen_in_bb933bd9e1b3bc0f:()=>z,__wbg___wbindgen_is_object_c818261d21f283a4:()=>L,__wbg___wbindgen_is_string_fbb76cb2940daafd:()=>P,__wbg___wbindgen_is_undefined_2d472862bd29a478:()=>Q,__wbg___wbindgen_jsval_loose_eq_b664b38a2f582147:()=>Y,__wbg___wbindgen_number_get_a20bf9b85341449d:()=>G,__wbg___wbindgen_string_get_e4f06c90489ad01b:()=>J,__wbg___wbindgen_throw_b855445ff6a94295:()=>X,__wbg_entries_e171b586f8f6bdbf:()=>H,__wbg_getTime_14776bfb48a1bff9:()=>K,__wbg_get_7bed016f185add81:()=>Z,__wbg_get_with_ref_key_1dc361bd10053bfe:()=>v,__wbg_instanceof_ArrayBuffer_70beb1189ca63b38:()=>ee,__wbg_instanceof_Uint8Array_20c8e73002f7af98:()=>te,__wbg_isSafeInteger_d216eda7911dde36:()=>ne,__wbg_length_69bca3cb64fc8748:()=>re,__wbg_length_cdd215e10d9dd507:()=>_e,__wbg_new_0_f9740686d739025c:()=>oe,__wbg_new_1acc0b6eea89d040:()=>ce,__wbg_new_5a79be3ab53b8aa5:()=>ie,__wbg_new_68651c719dcda04e:()=>se,__wbg_new_e17d9f43105b08be:()=>ue,__wbg_prototypesetcall_2a6620b6922694b2:()=>fe,__wbg_set_3f1d0b984ed272ed:()=>be,__wbg_set_907fb406c34a251d:()=>de,__wbg_set_c213c871859d6500:()=>ae,__wbg_set_message_82ae475bb413aa5c:()=>ge,__wbg_set_wasm:()=>N,__wbindgen_cast_2241b6af4c4b2941:()=>le,__wbindgen_cast_4625c577ab2ec9ee:()=>we,__wbindgen_cast_9ae0607507abb057:()=>pe,__wbindgen_cast_d6cd19b81560fd6e:()=>ye,__wbindgen_init_externref_table:()=>me});module.exports=B(xe);var A=()=>{};A.prototype=A;let o;function N(e){o=e}let p=null;function a(){return(p===null||p.byteLength===0)&&(p=new Uint8Array(o.memory.buffer)),p}let y=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});y.decode();const U=2146435072;let S=0;function R(e,t){return S+=t,S>=U&&(y=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),y.decode(),S=t),y.decode(a().subarray(e,e+t))}function m(e,t){return e=e>>>0,R(e,t)}let f=0;const g=new TextEncoder;"encodeInto"in g||(g.encodeInto=function(e,t){const n=g.encode(e);return t.set(n),{read:e.length,written:n.length}});function l(e,t,n){if(n===void 0){const i=g.encode(e),d=t(i.length,1)>>>0;return a().subarray(d,d+i.length).set(i),f=i.length,d}let _=e.length,r=t(_,1)>>>0;const s=a();let c=0;for(;c<_;c++){const i=e.charCodeAt(c);if(i>127)break;s[r+c]=i}if(c!==_){c!==0&&(e=e.slice(c)),r=n(r,_,_=c+e.length*3,1)>>>0;const i=a().subarray(r+c,r+_),d=g.encodeInto(e,i);c+=d.written,r=n(r,_,c,1)>>>0}return f=c,r}let b=null;function u(){return(b===null||b.buffer.detached===!0||b.buffer.detached===void 0&&b.buffer!==o.memory.buffer)&&(b=new DataView(o.memory.buffer)),b}function x(e){return e==null}function I(e){const t=typeof e;if(t=="number"||t=="boolean"||e==null)return`${e}`;if(t=="string")return`"${e}"`;if(t=="symbol"){const r=e.description;return r==null?"Symbol":`Symbol(${r})`}if(t=="function"){const r=e.name;return typeof r=="string"&&r.length>0?`Function(${r})`:"Function"}if(Array.isArray(e)){const r=e.length;let s="[";r>0&&(s+=I(e[0]));for(let c=1;c<r;c++)s+=", "+I(e[c]);return s+="]",s}const n=/\[object ([^\]]+)\]/.exec(toString.call(e));let _;if(n&&n.length>1)_=n[1];else return toString.call(e);if(_=="Object")try{return"Object("+JSON.stringify(e)+")"}catch{return"Object"}return e instanceof Error?`${e.name}: ${e.message}
${e.stack}`:_}function $(e,t){return e=e>>>0,a().subarray(e/1,e/1+t)}function w(e){const t=o.__wbindgen_externrefs.get(e);return o.__externref_table_dealloc(e),t}const E=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(e=>o.__wbg_querycompiler_free(e>>>0,1));class F{__destroy_into_raw(){const t=this.__wbg_ptr;return this.__wbg_ptr=0,E.unregister(this),t}free(){const t=this.__destroy_into_raw();o.__wbg_querycompiler_free(t,0)}compileBatch(t){const n=l(t,o.__wbindgen_malloc,o.__wbindgen_realloc),_=f,r=o.querycompiler_compileBatch(this.__wbg_ptr,n,_);if(r[2])throw w(r[1]);return w(r[0])}constructor(t){const n=o.querycompiler_new(t);if(n[2])throw w(n[1]);return this.__wbg_ptr=n[0]>>>0,E.register(this,this.__wbg_ptr,this),this}compile(t){const n=l(t,o.__wbindgen_malloc,o.__wbindgen_realloc),_=f,r=o.querycompiler_compile(this.__wbg_ptr,n,_);if(r[2])throw w(r[1]);return w(r[0])}}Symbol.dispose&&(F.prototype[Symbol.dispose]=F.prototype.free);function q(e,t){return Error(m(e,t))}function C(e){return Number(e)}function k(e,t){const n=String(t),_=l(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=f;u().setInt32(e+4*1,r,!0),u().setInt32(e+4*0,_,!0)}function W(e){const t=e,n=typeof t=="boolean"?t:void 0;return x(n)?16777215:n?1:0}function V(e,t){const n=I(t),_=l(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=f;u().setInt32(e+4*1,r,!0),u().setInt32(e+4*0,_,!0)}function z(e,t){return e in t}function L(e){const t=e;return typeof t=="object"&&t!==null}function P(e){return typeof e=="string"}function Q(e){return e===void 0}function Y(e,t){return e==t}function G(e,t){const n=t,_=typeof n=="number"?n:void 0;u().setFloat64(e+8*1,x(_)?0:_,!0),u().setInt32(e+4*0,!x(_),!0)}function J(e,t){const n=t,_=typeof n=="string"?n:void 0;var r=x(_)?0:l(_,o.__wbindgen_malloc,o.__wbindgen_realloc),s=f;u().setInt32(e+4*1,s,!0),u().setInt32(e+4*0,r,!0)}function X(e,t){throw new Error(m(e,t))}function H(e){return Object.entries(e)}function K(e){return e.getTime()}function Z(e,t){return e[t>>>0]}function v(e,t){return e[t]}function ee(e){let t;try{t=e instanceof ArrayBuffer}catch{t=!1}return t}function te(e){let t;try{t=e instanceof Uint8Array}catch{t=!1}return t}function ne(e){return Number.isSafeInteger(e)}function re(e){return e.length}function _e(e){return e.length}function oe(){return new Date}function ce(){return new Object}function ie(e){return new Uint8Array(e)}function se(){return new Map}function ue(){return new Array}function fe(e,t,n){Uint8Array.prototype.set.call($(e,t),n)}function be(e,t,n){e[t]=n}function de(e,t,n){return e.set(t,n)}function ae(e,t,n){e[t>>>0]=n}function ge(e,t){global.PRISMA_WASM_PANIC_REGISTRY.set_message(m(e,t))}function le(e,t){return m(e,t)}function we(e){return BigInt.asUintN(64,e)}function pe(e){return e}function ye(e){return e}function me(){const e=o.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}0&&(module.exports={QueryCompiler,__wbg_Error_e83987f665cf5504,__wbg_Number_bb48ca12f395cd08,__wbg_String_8f0eb39a4a4c2f66,__wbg___wbindgen_boolean_get_6d5a1ee65bab5f68,__wbg___wbindgen_debug_string_df47ffb5e35e6763,__wbg___wbindgen_in_bb933bd9e1b3bc0f,__wbg___wbindgen_is_object_c818261d21f283a4,__wbg___wbindgen_is_string_fbb76cb2940daafd,__wbg___wbindgen_is_undefined_2d472862bd29a478,__wbg___wbindgen_jsval_loose_eq_b664b38a2f582147,__wbg___wbindgen_number_get_a20bf9b85341449d,__wbg___wbindgen_string_get_e4f06c90489ad01b,__wbg___wbindgen_throw_b855445ff6a94295,__wbg_entries_e171b586f8f6bdbf,__wbg_getTime_14776bfb48a1bff9,__wbg_get_7bed016f185add81,__wbg_get_with_ref_key_1dc361bd10053bfe,__wbg_instanceof_ArrayBuffer_70beb1189ca63b38,__wbg_instanceof_Uint8Array_20c8e73002f7af98,__wbg_isSafeInteger_d216eda7911dde36,__wbg_length_69bca3cb64fc8748,__wbg_length_cdd215e10d9dd507,__wbg_new_0_f9740686d739025c,__wbg_new_1acc0b6eea89d040,__wbg_new_5a79be3ab53b8aa5,__wbg_new_68651c719dcda04e,__wbg_new_e17d9f43105b08be,__wbg_prototypesetcall_2a6620b6922694b2,__wbg_set_3f1d0b984ed272ed,__wbg_set_907fb406c34a251d,__wbg_set_c213c871859d6500,__wbg_set_message_82ae475bb413aa5c,__wbg_set_wasm,__wbindgen_cast_2241b6af4c4b2941,__wbindgen_cast_4625c577ab2ec9ee,__wbindgen_cast_9ae0607507abb057,__wbindgen_cast_d6cd19b81560fd6e,__wbindgen_init_externref_table});
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+90
View File
@@ -0,0 +1,90 @@
import { AnyNull } from '@prisma/client-runtime-utils';
import { DbNull } from '@prisma/client-runtime-utils';
import { Decimal } from '@prisma/client-runtime-utils';
import { isAnyNull } from '@prisma/client-runtime-utils';
import { isDbNull } from '@prisma/client-runtime-utils';
import { isJsonNull } from '@prisma/client-runtime-utils';
import { isObjectEnumValue } from '@prisma/client-runtime-utils';
import { JsonNull } from '@prisma/client-runtime-utils';
import { NullTypes } from '@prisma/client-runtime-utils';
export { AnyNull }
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
export { DbNull }
export { Decimal }
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: RuntimeName;
prettyName: string;
isEdge: boolean;
};
export { isAnyNull }
export { isDbNull }
export { isJsonNull }
export { isObjectEnumValue }
export { JsonNull }
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
export { NullTypes }
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }
@@ -0,0 +1,6 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
"use strict";var s=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var f=Object.prototype.hasOwnProperty;var a=(e,t)=>{for(var n in t)s(e,n,{get:t[n],enumerable:!0})},y=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of p(t))!f.call(e,i)&&i!==n&&s(e,i,{get:()=>t[i],enumerable:!(r=g(t,i))||r.enumerable});return e};var x=e=>y(s({},"__esModule",{value:!0}),e);var M={};a(M,{AnyNull:()=>o.AnyNull,DbNull:()=>o.DbNull,Decimal:()=>m.Decimal,JsonNull:()=>o.JsonNull,NullTypes:()=>o.NullTypes,Public:()=>l,getRuntime:()=>c,isAnyNull:()=>o.isAnyNull,isDbNull:()=>o.isDbNull,isJsonNull:()=>o.isJsonNull,isObjectEnumValue:()=>o.isObjectEnumValue,makeStrictEnum:()=>u});module.exports=x(M);var l={};a(l,{validator:()=>d});function d(...e){return t=>t}var b=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function u(e){return new Proxy(e,{get(t,n){if(n in t)return t[n];if(!b.has(n))throw new TypeError("Invalid enum value: ".concat(String(n)))}})}var N=()=>{var e,t;return((t=(e=globalThis.process)==null?void 0:e.release)==null?void 0:t.name)==="node"},E=()=>{var e,t;return!!globalThis.Bun||!!((t=(e=globalThis.process)==null?void 0:e.versions)!=null&&t.bun)},S=()=>!!globalThis.Deno,R=()=>typeof globalThis.Netlify=="object",h=()=>typeof globalThis.EdgeRuntime=="object",C=()=>{var e;return((e=globalThis.navigator)==null?void 0:e.userAgent)==="Cloudflare-Workers"};function k(){var n;return(n=[[R,"netlify"],[h,"edge-light"],[C,"workerd"],[S,"deno"],[E,"bun"],[N,"node"]].flatMap(r=>r[0]()?[r[1]]:[]).at(0))!=null?n:""}var O={node:"Node.js",workerd:"Cloudflare Workers",deno:"Deno and Deno Deploy",netlify:"Netlify Edge Functions","edge-light":"Edge Runtime (Vercel Edge Functions, Vercel Edge Middleware, Next.js (Pages Router) Edge API Routes, Next.js (App Router) Edge Route Handlers or Next.js Middleware)"};function c(){let e=k();return{id:e,prettyName:O[e]||e,isEdge:["workerd","deno","netlify","edge-light"].includes(e)}}var o=require("@prisma/client-runtime-utils"),m=require("@prisma/client-runtime-utils");0&&(module.exports={AnyNull,DbNull,Decimal,JsonNull,NullTypes,Public,getRuntime,isAnyNull,isDbNull,isJsonNull,isObjectEnumValue,makeStrictEnum});
//# sourceMappingURL=index-browser.js.map
File diff suppressed because one or more lines are too long
+41
View File
@@ -0,0 +1,41 @@
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
enum VoiceExperienceStatus {
UPLOADED
TRANSCRIBING
TRANSCRIBED
ANALYZING
ANALYZED
FAILED
}
model Place {
id String @id @default(cuid())
googlePlaceId String @unique
name String
latitude Float
longitude Float
experiences VoiceExperience[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VoiceExperience {
id String @id @default(cuid())
placeId String
place Place @relation(fields: [placeId], references: [id])
durationSeconds Int
audioObjectKey String
status VoiceExperienceStatus @default(UPLOADED)
transcript String?
analysis Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_compiler_fast_bg.wasm?module')
@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_compiler_fast_bg.wasm')
+8
View File
@@ -0,0 +1,8 @@
export const GraphQLJSONObject = {
__serialize(value: unknown) {
return value;
},
__parseValue(value: unknown) {
return value;
},
};
+64
View File
@@ -0,0 +1,64 @@
import { GraphQLJSONObject } from './scalars.js';
import { createVoiceExperience } from './voice-experiences.js';
export const schema = /* GraphQL */ `
scalar JSON
enum VoiceExperienceStatus {
UPLOADED
TRANSCRIBING
TRANSCRIBED
ANALYZING
ANALYZED
FAILED
}
type Place {
id: ID!
googlePlaceId: String!
name: String!
latitude: Float!
longitude: Float!
}
type VoiceExperience {
id: ID!
place: Place!
status: VoiceExperienceStatus!
durationSeconds: Int!
audioObjectKey: String!
transcript: String
analysis: JSON
createdAt: String!
}
input CreateVoiceExperienceInput {
googlePlaceId: String!
googleName: String!
latitude: Float!
longitude: Float!
durationSeconds: Int!
audioObjectKey: String!
}
type Query {
health: String!
}
type Mutation {
createVoiceExperience(input: CreateVoiceExperienceInput!): VoiceExperience!
}
`;
export const resolvers = {
JSON: GraphQLJSONObject,
Query: {
health: () => 'ok',
},
Mutation: {
createVoiceExperience: async (
_: unknown,
args: { input: Parameters<typeof createVoiceExperience>[0] },
) => createVoiceExperience(args.input),
},
};
+46
View File
@@ -0,0 +1,46 @@
import { enqueueVoiceExperience } from '../hatchet/enqueue-voice-experience.js';
import { prisma } from '../prisma.js';
export type CreateVoiceExperienceInput = {
googlePlaceId: string;
googleName: string;
latitude: number;
longitude: number;
durationSeconds: number;
audioObjectKey: string;
};
export async function createVoiceExperience(input: CreateVoiceExperienceInput) {
if (input.durationSeconds < 60) {
throw new Error('Voice experience must be at least 60 seconds.');
}
const place = await prisma.place.upsert({
where: { googlePlaceId: input.googlePlaceId },
create: {
googlePlaceId: input.googlePlaceId,
name: input.googleName,
latitude: input.latitude,
longitude: input.longitude,
},
update: {
name: input.googleName,
latitude: input.latitude,
longitude: input.longitude,
},
});
const experience = await prisma.voiceExperience.create({
data: {
placeId: place.id,
durationSeconds: input.durationSeconds,
audioObjectKey: input.audioObjectKey,
status: 'UPLOADED',
},
include: { place: true },
});
await enqueueVoiceExperience({ experienceId: experience.id });
return experience;
}
+12
View File
@@ -0,0 +1,12 @@
import { hatchet } from './hatchet-client.js';
import { workflowNames } from './workflow-names.js';
export type ProcessVoiceExperienceInput = {
experienceId: string;
};
export async function enqueueVoiceExperience(
input: ProcessVoiceExperienceInput,
) {
await hatchet.admin.runWorkflow(workflowNames.processVoiceExperience, input);
}
+7
View File
@@ -0,0 +1,7 @@
import { HatchetClient } from '@hatchet-dev/typescript-sdk';
import { config } from '../config.js';
export const hatchet = HatchetClient.init({
token: config.hatchetToken,
});
+3
View File
@@ -0,0 +1,3 @@
export const workflowNames = {
processVoiceExperience: 'process_voice_experience',
} as const;
+8
View File
@@ -0,0 +1,8 @@
import { PrismaPg } from '@prisma/adapter-pg';
import { config } from './config.js';
import { PrismaClient } from './generated/prisma/client.js';
const adapter = new PrismaPg({ connectionString: config.databaseUrl });
export const prisma = new PrismaClient({ adapter });
+22
View File
@@ -0,0 +1,22 @@
import Fastify from 'fastify';
import mercurius from 'mercurius';
import { config } from './config.js';
import { prisma } from './prisma.js';
import { resolvers, schema } from './graphql/schema.js';
const app = Fastify({ logger: true });
app.register(mercurius, {
schema,
resolvers,
graphiql: true,
});
app.get('/health', async () => ({ ok: true }));
app.addHook('onClose', async () => {
await prisma.$disconnect();
});
await app.listen({ host: config.host, port: config.port });
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}