diff --git a/apps/server-new/src/domain/models.ts b/apps/server-new/src/domain/models.ts index 556a5064..ad88fb2b 100644 --- a/apps/server-new/src/domain/models.ts +++ b/apps/server-new/src/domain/models.ts @@ -20,7 +20,7 @@ export const InboxSenderAuthPolicy = Schema.Literal('requires_auth', 'anonymous' /** * Database entity schemas (Prisma-based) */ -export const Account = Schema.Struct({ +export class Account extends Schema.Class('Account')({ address: Schema.String, connectAddress: Schema.String, connectCiphertext: Schema.String, @@ -30,9 +30,9 @@ export const Account = Schema.Struct({ connectAccountProof: Schema.String, connectKeyProof: Schema.String, connectSignerAddress: Schema.String, -}); +}) {} -export const AppIdentity = Schema.Struct({ +export class AppIdentity extends Schema.Class('AppIdentity')({ address: Schema.String, ciphertext: Schema.String, signaturePublicKey: Schema.String, @@ -42,34 +42,34 @@ export const AppIdentity = Schema.Struct({ accountAddress: Schema.String, appId: Schema.String, sessionToken: Schema.String.pipe(Schema.NullOr), - sessionTokenExpires: Schema.DateFromSelf.pipe(Schema.NullOr), -}); + sessionTokenExpires: Schema.Date.pipe(Schema.NullOr), +}) {} -export const Space = Schema.Struct({ +export class Space extends Schema.Class('Space')({ id: Schema.String, name: Schema.String, infoContent: Schema.Uint8Array, infoAuthorAddress: Schema.String, infoSignatureHex: Schema.String, infoSignatureRecovery: Schema.Number, -}); +}) {} -export const SpaceEvent = Schema.Struct({ +export class SpaceEvent extends Schema.Class('SpaceEvent')({ id: Schema.String, event: Schema.String, state: Schema.String, counter: Schema.Number, spaceId: Schema.String, createdAt: Schema.DateFromSelf, -}); +}) {} -export const SpaceKey = Schema.Struct({ +export class SpaceKey extends Schema.Class('SpaceKey')({ id: Schema.String, spaceId: Schema.String, createdAt: Schema.DateFromSelf, -}); +}) {} -export const SpaceKeyBox = Schema.Struct({ +export class SpaceKeyBox extends Schema.Class('SpaceKeyBox')({ id: Schema.String, spaceKeyId: Schema.String, ciphertext: Schema.String, @@ -78,9 +78,9 @@ export const SpaceKeyBox = Schema.Struct({ accountAddress: Schema.String, appIdentityAddress: Schema.optional(Schema.String), createdAt: Schema.DateFromSelf, -}); +}) {} -export const Update = Schema.Struct({ +export class Update extends Schema.Class('Update')({ spaceId: Schema.String, clock: Schema.Number, content: Schema.Uint8Array, @@ -88,9 +88,9 @@ export const Update = Schema.Struct({ signatureHex: Schema.String, signatureRecovery: Schema.Number, updateId: Schema.String, -}); +}) {} -export const SpaceInbox = Schema.Struct({ +export class SpaceInbox extends Schema.Class('SpaceInbox')({ id: Schema.String, spaceId: Schema.String, isPublic: Schema.Boolean, @@ -99,9 +99,9 @@ export const SpaceInbox = Schema.Struct({ encryptedSecretKey: Schema.String, spaceEventId: Schema.String, createdAt: Schema.DateFromSelf, -}); +}) {} -export const SpaceInboxMessage = Schema.Struct({ +export class SpaceInboxMessage extends Schema.Class('SpaceInboxMessage')({ id: Schema.String, spaceInboxId: Schema.String, ciphertext: Schema.String, @@ -109,9 +109,9 @@ export const SpaceInboxMessage = Schema.Struct({ signatureRecovery: Schema.optional(Schema.Number), authorAccountAddress: Schema.optional(Schema.String), createdAt: Schema.DateFromSelf, -}); +}) {} -export const AccountInbox = Schema.Struct({ +export class AccountInbox extends Schema.Class('AccountInbox')({ id: Schema.String, accountAddress: Schema.String, isPublic: Schema.Boolean, @@ -120,9 +120,9 @@ export const AccountInbox = Schema.Struct({ signatureHex: Schema.String, signatureRecovery: Schema.Number, createdAt: Schema.DateFromSelf, -}); +}) {} -export const AccountInboxMessage = Schema.Struct({ +export class AccountInboxMessage extends Schema.Class('AccountInboxMessage')({ id: Schema.String, accountInboxId: Schema.String, ciphertext: Schema.String, @@ -130,45 +130,45 @@ export const AccountInboxMessage = Schema.Struct({ signatureRecovery: Schema.optional(Schema.Number), authorAccountAddress: Schema.optional(Schema.String), createdAt: Schema.DateFromSelf, -}); +}) {} -export const Invitation = Schema.Struct({ +export class Invitation extends Schema.Class('Invitation')({ id: Schema.String, spaceId: Schema.String, accountAddress: Schema.String, inviteeAccountAddress: Schema.String, createdAt: Schema.DateFromSelf, -}); +}) {} -export const InvitationTargetApp = Schema.Struct({ +export class InvitationTargetApp extends Schema.Class('InvitationTargetApp')({ id: Schema.String, invitationId: Schema.String, -}); +}) {} /** * API response schemas */ -export const SpaceInboxPublic = Schema.Struct({ +export class SpaceInboxPublic extends Schema.Class('SpaceInboxPublic')({ id: Schema.String, spaceId: Schema.String, isPublic: Schema.Boolean, authPolicy: InboxSenderAuthPolicy, encryptionPublicKey: Schema.String, -}); +}) {} -export const AccountInboxPublic = Schema.Struct({ +export class AccountInboxPublic extends Schema.Class('AccountInboxPublic')({ id: Schema.String, accountAddress: Schema.String, isPublic: Schema.Boolean, authPolicy: InboxSenderAuthPolicy, encryptionPublicKey: Schema.String, -}); +}) {} -export const PublicIdentity = Schema.Struct({ +export class PublicIdentity extends Schema.Class('PublicIdentity')({ accountAddress: Schema.String, signaturePublicKey: Schema.String, encryptionPublicKey: Schema.String, accountProof: Schema.String, keyProof: Schema.String, appId: Schema.optional(Schema.String), -}); +}) {} diff --git a/apps/server-new/src/server.ts b/apps/server-new/src/server.ts index fdc13e28..a6cbf865 100644 --- a/apps/server-new/src/server.ts +++ b/apps/server-new/src/server.ts @@ -1,21 +1,50 @@ import { createServer } from 'node:http'; -import { HttpApiBuilder, HttpServer } from '@effect/platform'; -import { NodeHttpServer } from '@effect/platform-node'; -import { Effect, Layer } from 'effect'; +import * as HttpApiScalar from '@effect/platform/HttpApiScalar'; +import * as HttpLayerRouter from '@effect/platform/HttpLayerRouter'; +import * as HttpMiddleware from '@effect/platform/HttpMiddleware'; +import * as HttpServerRequest from '@effect/platform/HttpServerRequest'; +import * as HttpServerResponse from '@effect/platform/HttpServerResponse'; +import * as NodeHttpServer from '@effect/platform-node/NodeHttpServer'; +import * as Effect from 'effect/Effect'; +import * as Layer from 'effect/Layer'; +import * as Schedule from 'effect/Schedule'; +import * as Stream from 'effect/Stream'; import { serverPortConfig } from './config/server.ts'; import { hypergraphApi } from './http/api.ts'; import { HandlersLive } from './http/handlers.ts'; -const apiLive = HttpApiBuilder.api(hypergraphApi).pipe(Layer.provide(HandlersLive)); +// Create scalar openapi browser layer at /docs. +const DocsLayer = HttpApiScalar.layerHttpLayerRouter({ + api: hypergraphApi, + path: '/docs', +}); -export const server = Layer.unwrapEffect( - Effect.gen(function* () { - const port = yield* serverPortConfig; - return HttpApiBuilder.serve().pipe( - Layer.provide(HttpApiBuilder.middlewareCors()), - Layer.provide(apiLive), - HttpServer.withLogAddress, - Layer.provide(NodeHttpServer.layer(createServer, { port })), - ); - }), +// Create api layer with openapi.json documentation generated at /docs/openapi.json. +const ApiLayer = HttpLayerRouter.addHttpApi(hypergraphApi, { + openapiPath: '/docs/openapi.json', +}).pipe(Layer.provide(HandlersLive)); + +// Create websocket layer at /ws. +const WebSocketLayer = HttpLayerRouter.add( + 'GET', + '/ws', + // TODO: Implement actual websocket logic here. + Stream.fromSchedule(Schedule.spaced(1000)).pipe( + Stream.map(JSON.stringify), + Stream.pipeThroughChannel(HttpServerRequest.upgradeChannel()), + Stream.decodeText(), + Stream.runForEach((_) => Effect.log(_)), + Effect.as(HttpServerResponse.empty()), + ), ); + +// Merge router layers together and add the cors middleware layer. +const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors()); +const AppLayer = Layer.mergeAll(ApiLayer, DocsLayer, WebSocketLayer).pipe(Layer.provide(CorsMiddleware.layer)); + +const HttpServerLayer = serverPortConfig.pipe( + Effect.map((port) => NodeHttpServer.layer(createServer, { port })), + Layer.unwrapEffect, +); + +export const server = HttpLayerRouter.serve(AppLayer).pipe(Layer.provide(HttpServerLayer));