lecodes-sdk 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/dist/global.d.ts +503 -0
- package/dist/host.d.ts +61 -0
- package/dist/inject.js +4730 -0
- package/dist/types/animate/animate.d.ts +20 -0
- package/dist/types/animate/bezier.d.ts +2 -0
- package/dist/types/animate/easings.d.ts +21 -0
- package/dist/types/canvas/Canvas.d.ts +168 -0
- package/dist/types/core/Aspect.d.ts +171 -0
- package/dist/types/core/InspectorUI.d.ts +88 -0
- package/dist/types/core/StateMachine.d.ts +81 -0
- package/dist/types/core/color.d.ts +13 -0
- package/dist/types/core/compWrite.d.ts +11 -0
- package/dist/types/core/events.d.ts +11 -0
- package/dist/types/core/fields.d.ts +58 -0
- package/dist/types/core/registry.d.ts +7 -0
- package/dist/types/core/signals.d.ts +29 -0
- package/dist/types/core/time.d.ts +31 -0
- package/dist/types/g2/Camera2D.d.ts +19 -0
- package/dist/types/g2/CharacterController2D.d.ts +134 -0
- package/dist/types/g2/Node2D.d.ts +91 -0
- package/dist/types/g2/OneWay2D.d.ts +24 -0
- package/dist/types/g2/Physics2D.d.ts +147 -0
- package/dist/types/g2/Scene2D.d.ts +72 -0
- package/dist/types/g2/Shape2D.d.ts +58 -0
- package/dist/types/g2/Sprite.d.ts +51 -0
- package/dist/types/g2/SpriteAnimation.d.ts +50 -0
- package/dist/types/g2/SpriteSheet.d.ts +69 -0
- package/dist/types/g2/Texture2D.d.ts +16 -0
- package/dist/types/g2/Tilemap.d.ts +20 -0
- package/dist/types/g2/Tileset.d.ts +32 -0
- package/dist/types/g2/Trigger2D.d.ts +20 -0
- package/dist/types/g2/autotile.d.ts +133 -0
- package/dist/types/g2/cells.d.ts +14 -0
- package/dist/types/g2/defineScene2d.d.ts +141 -0
- package/dist/types/g2/groups2d.d.ts +16 -0
- package/dist/types/g2/loop.d.ts +4 -0
- package/dist/types/g2/scenarios2d.d.ts +30 -0
- package/dist/types/g2/touch.d.ts +2 -0
- package/dist/types/gl/Camera.d.ts +79 -0
- package/dist/types/gl/CameraPlace.d.ts +26 -0
- package/dist/types/gl/CharacterController.d.ts +98 -0
- package/dist/types/gl/Gearbox.d.ts +86 -0
- package/dist/types/gl/Geometry.d.ts +39 -0
- package/dist/types/gl/IK.d.ts +53 -0
- package/dist/types/gl/InstancedMesh.d.ts +46 -0
- package/dist/types/gl/Light.d.ts +73 -0
- package/dist/types/gl/Lightmap.d.ts +85 -0
- package/dist/types/gl/Locomotion.d.ts +279 -0
- package/dist/types/gl/Material.d.ts +100 -0
- package/dist/types/gl/Mesh.d.ts +33 -0
- package/dist/types/gl/Model.d.ts +53 -0
- package/dist/types/gl/NavAgent.d.ts +99 -0
- package/dist/types/gl/NavMesh.d.ts +169 -0
- package/dist/types/gl/Node.d.ts +99 -0
- package/dist/types/gl/Noise.d.ts +20 -0
- package/dist/types/gl/Particles.d.ts +277 -0
- package/dist/types/gl/Physics.d.ts +91 -0
- package/dist/types/gl/Plane.d.ts +19 -0
- package/dist/types/gl/Ragdoll.d.ts +86 -0
- package/dist/types/gl/Ray.d.ts +8 -0
- package/dist/types/gl/Scene.d.ts +221 -0
- package/dist/types/gl/Shape.d.ts +122 -0
- package/dist/types/gl/Terrain.d.ts +280 -0
- package/dist/types/gl/Texture.d.ts +27 -0
- package/dist/types/gl/Trigger.d.ts +10 -0
- package/dist/types/gl/Vehicle.d.ts +191 -0
- package/dist/types/gl/Wheel.d.ts +95 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
- package/dist/types/gl/animation/Animator.d.ts +219 -0
- package/dist/types/gl/animation/Layer.d.ts +31 -0
- package/dist/types/gl/animation/Loop.d.ts +17 -0
- package/dist/types/gl/animation/Playback.d.ts +36 -0
- package/dist/types/gl/animation/core.d.ts +367 -0
- package/dist/types/gl/controls.d.ts +19 -0
- package/dist/types/gl/physicsEvents.d.ts +1 -0
- package/dist/types/gl/scenarios.d.ts +98 -0
- package/dist/types/gl/state.d.ts +5 -0
- package/dist/types/gl/terrainMesh.d.ts +46 -0
- package/dist/types/gl/touch.d.ts +2 -0
- package/dist/types/inject.d.ts +123 -0
- package/dist/types/math/Mathf.d.ts +39 -0
- package/dist/types/math/mat4.d.ts +66 -0
- package/dist/types/math/quat.d.ts +53 -0
- package/dist/types/math/vec.d.ts +129 -0
- package/dist/types/net/codec.d.ts +25 -0
- package/dist/types/net/core.d.ts +106 -0
- package/dist/types/net/index.d.ts +80 -0
- package/dist/types/net/replication.d.ts +101 -0
- package/dist/types/plugins/camera.d.ts +25 -0
- package/dist/types/plugins/geolocation.d.ts +39 -0
- package/dist/types/plugins/oauth.d.ts +25 -0
- package/dist/types/plugins/permission.d.ts +1 -0
- package/dist/types/plugins/push.d.ts +46 -0
- package/dist/types/plugins/qr.d.ts +21 -0
- package/dist/types/plugins/service.d.ts +33 -0
- package/dist/types/runtime/app.d.ts +67 -0
- package/dist/types/runtime/appEvents.d.ts +7 -0
- package/dist/types/runtime/channel.d.ts +1 -0
- package/dist/types/runtime/clipboard.d.ts +7 -0
- package/dist/types/runtime/datetime.d.ts +80 -0
- package/dist/types/runtime/device.d.ts +126 -0
- package/dist/types/runtime/fetch.d.ts +31 -0
- package/dist/types/runtime/files.d.ts +33 -0
- package/dist/types/runtime/input.d.ts +121 -0
- package/dist/types/runtime/media.d.ts +46 -0
- package/dist/types/runtime/misc.d.ts +11 -0
- package/dist/types/runtime/net.d.ts +15 -0
- package/dist/types/runtime/rpc.d.ts +44 -0
- package/dist/types/runtime/service.d.ts +1 -0
- package/dist/types/runtime/share.d.ts +2 -0
- package/dist/types/runtime/storage.d.ts +5 -0
- package/dist/types/runtime/touch.d.ts +56 -0
- package/dist/types/scene/defineScene.d.ts +296 -0
- package/dist/types/scene/editorPlugins.d.ts +82 -0
- package/dist/types/scene/gizmos.d.ts +56 -0
- package/dist/types/scene/grammar.d.ts +56 -0
- package/dist/types/scene/level.d.ts +98 -0
- package/dist/types/scene/material.d.ts +76 -0
- package/dist/types/server/auth/appConfig.d.ts +16 -0
- package/dist/types/server/auth/global.d.ts +56 -0
- package/dist/types/server/auth/models.d.ts +164 -0
- package/dist/types/server/auth/types.d.ts +56 -0
- package/dist/types/server/channel.d.ts +28 -0
- package/dist/types/server/context.d.ts +27 -0
- package/dist/types/server/db/defineDb.d.ts +36 -0
- package/dist/types/server/db/fields.d.ts +146 -0
- package/dist/types/server/db/index.d.ts +6 -0
- package/dist/types/server/db/marci/query.d.ts +326 -0
- package/dist/types/server/db/types.d.ts +163 -0
- package/dist/types/server/errors.d.ts +8 -0
- package/dist/types/server/inject.d.ts +5 -0
- package/dist/types/ui/NativeView.d.ts +38 -0
- package/dist/types/ui/UI.d.ts +27 -0
- package/dist/types/ui/UIBottomSheet.d.ts +71 -0
- package/dist/types/ui/UIButton.d.ts +30 -0
- package/dist/types/ui/UIContainer.d.ts +33 -0
- package/dist/types/ui/UIImage.d.ts +35 -0
- package/dist/types/ui/UIInput.d.ts +110 -0
- package/dist/types/ui/UIModal.d.ts +70 -0
- package/dist/types/ui/UINode.d.ts +363 -0
- package/dist/types/ui/UIPager.d.ts +123 -0
- package/dist/types/ui/UIPopover.d.ts +39 -0
- package/dist/types/ui/UIScreen.d.ts +57 -0
- package/dist/types/ui/UIScrollable.d.ts +40 -0
- package/dist/types/ui/UISpacer.d.ts +9 -0
- package/dist/types/ui/UITabs.d.ts +96 -0
- package/dist/types/ui/UIText.d.ts +19 -0
- package/dist/types/ui/UIVideo.d.ts +28 -0
- package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
- package/dist/types/ui/UIWidget.d.ts +65 -0
- package/dist/types/ui/fonts.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +68 -0
- package/dist/types/ui/router.d.ts +40 -0
- package/dist/types/ui/theme.d.ts +26 -0
- package/dist/types.json +1 -0
- package/package.json +46 -0
- package/prompts/2d.md +395 -0
- package/prompts/3d-scene-files.md +113 -0
- package/prompts/3d-scene.md +119 -0
- package/prompts/3d.md +303 -0
- package/prompts/README.md +142 -0
- package/prompts/ar.md +109 -0
- package/prompts/canvas.md +27 -0
- package/prompts/compose.ts +46 -0
- package/prompts/concept.md +49 -0
- package/prompts/core-design.md +75 -0
- package/prompts/core.md +264 -0
- package/prompts/design.md +218 -0
- package/prompts/dist/2d-game.md +1530 -0
- package/prompts/dist/3d-app.md +1672 -0
- package/prompts/dist/ar-app.md +1548 -0
- package/prompts/dist/concept.md +49 -0
- package/prompts/dist/design.md +498 -0
- package/prompts/dist/ui-app.md +1134 -0
- package/prompts/index.md +37 -0
- package/prompts/intake-prompt.md +33 -0
- package/prompts/namer-prompt.md +12 -0
- package/prompts/router-prompt.md +50 -0
- package/prompts/select.ts +111 -0
- package/prompts/ui-design.md +203 -0
- package/prompts/ui.md +803 -0
- package/src/animate/animate.ts +238 -0
- package/src/animate/bezier.ts +138 -0
- package/src/animate/easings.ts +126 -0
- package/src/bridges.d.ts +1345 -0
- package/src/canvas/Canvas.ts +305 -0
- package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
- package/src/compile/__tests__/assetMacro.test.ts +74 -0
- package/src/compile/__tests__/assetName.test.ts +55 -0
- package/src/compile/__tests__/compile.test.ts +287 -0
- package/src/compile/__tests__/detectEntry.test.ts +132 -0
- package/src/compile/__tests__/fontMacro.test.ts +199 -0
- package/src/compile/aspectMacro.ts +86 -0
- package/src/compile/assetIconMacro.ts +384 -0
- package/src/compile/assetMacro.ts +146 -0
- package/src/compile/assetName.ts +50 -0
- package/src/compile/bundler.ts +287 -0
- package/src/compile/compileProject.ts +127 -0
- package/src/compile/detectEntry.ts +128 -0
- package/src/compile/fontMacro.ts +459 -0
- package/src/compile/fontRegistry.ts +78 -0
- package/src/compile/header.ts +67 -0
- package/src/compile/index.ts +100 -0
- package/src/compile/libraryImports.ts +52 -0
- package/src/compile/liteMaterial.ts +247 -0
- package/src/compile/sceneEditor.ts +88 -0
- package/src/compile/serverSplit.ts +233 -0
- package/src/compile/serverTypes.ts +227 -0
- package/src/compile/sfnt.ts +98 -0
- package/src/compile/shaderSchema.ts +202 -0
- package/src/compile/shaderTargets.ts +77 -0
- package/src/compile/sourcemap.ts +25 -0
- package/src/core/Aspect.ts +568 -0
- package/src/core/InspectorUI.ts +212 -0
- package/src/core/StateMachine.ts +308 -0
- package/src/core/__tests__/stateMachine.test.ts +132 -0
- package/src/core/color.ts +66 -0
- package/src/core/compWrite.ts +42 -0
- package/src/core/events.ts +38 -0
- package/src/core/fields.ts +120 -0
- package/src/core/registry.ts +23 -0
- package/src/core/signals.ts +277 -0
- package/src/core/time.ts +84 -0
- package/src/g2/Camera2D.ts +40 -0
- package/src/g2/CharacterController2D.ts +276 -0
- package/src/g2/Node2D.ts +267 -0
- package/src/g2/OneWay2D.ts +66 -0
- package/src/g2/Physics2D.ts +346 -0
- package/src/g2/Scene2D.ts +209 -0
- package/src/g2/Shape2D.ts +259 -0
- package/src/g2/Sprite.ts +89 -0
- package/src/g2/SpriteAnimation.ts +171 -0
- package/src/g2/SpriteSheet.ts +166 -0
- package/src/g2/Texture2D.ts +47 -0
- package/src/g2/Tilemap.ts +41 -0
- package/src/g2/Tileset.ts +71 -0
- package/src/g2/Trigger2D.ts +77 -0
- package/src/g2/autotile.ts +433 -0
- package/src/g2/cells.ts +91 -0
- package/src/g2/defineScene2d.ts +381 -0
- package/src/g2/groups2d.ts +106 -0
- package/src/g2/loop.ts +50 -0
- package/src/g2/scenarios2d.ts +69 -0
- package/src/g2/touch.ts +83 -0
- package/src/gl/Camera.ts +160 -0
- package/src/gl/CameraPlace.ts +52 -0
- package/src/gl/CharacterController.ts +238 -0
- package/src/gl/Gearbox.ts +212 -0
- package/src/gl/Geometry.ts +279 -0
- package/src/gl/IK.ts +193 -0
- package/src/gl/InstancedMesh.ts +132 -0
- package/src/gl/Light.ts +135 -0
- package/src/gl/Lightmap.ts +402 -0
- package/src/gl/Locomotion.ts +498 -0
- package/src/gl/Material.ts +279 -0
- package/src/gl/Mesh.ts +83 -0
- package/src/gl/Model.ts +124 -0
- package/src/gl/NavAgent.ts +337 -0
- package/src/gl/NavMesh.ts +397 -0
- package/src/gl/Node.ts +350 -0
- package/src/gl/Noise.ts +30 -0
- package/src/gl/Particles.ts +676 -0
- package/src/gl/Physics.ts +222 -0
- package/src/gl/Plane.ts +53 -0
- package/src/gl/Ragdoll.ts +270 -0
- package/src/gl/Ray.ts +16 -0
- package/src/gl/Scene.ts +505 -0
- package/src/gl/Shape.ts +409 -0
- package/src/gl/Terrain.ts +1092 -0
- package/src/gl/Texture.ts +63 -0
- package/src/gl/Trigger.ts +45 -0
- package/src/gl/Vehicle.ts +473 -0
- package/src/gl/Wheel.ts +240 -0
- package/src/gl/animation/AnimationClip.ts +204 -0
- package/src/gl/animation/Animator.ts +329 -0
- package/src/gl/animation/Layer.ts +39 -0
- package/src/gl/animation/Loop.ts +31 -0
- package/src/gl/animation/Playback.ts +52 -0
- package/src/gl/animation/core.ts +670 -0
- package/src/gl/controls.ts +95 -0
- package/src/gl/physicsEvents.ts +20 -0
- package/src/gl/scenarios.ts +291 -0
- package/src/gl/state.ts +6 -0
- package/src/gl/terrainMesh.ts +219 -0
- package/src/gl/touch.ts +68 -0
- package/src/host.d.ts +61 -0
- package/src/inject.ts +203 -0
- package/src/math/Mathf.ts +118 -0
- package/src/math/mat4.ts +278 -0
- package/src/math/quat.ts +232 -0
- package/src/math/vec.ts +255 -0
- package/src/net/codec.ts +119 -0
- package/src/net/core.ts +384 -0
- package/src/net/index.ts +181 -0
- package/src/net/replication.ts +622 -0
- package/src/plugins/camera.ts +81 -0
- package/src/plugins/geolocation.ts +123 -0
- package/src/plugins/oauth.ts +61 -0
- package/src/plugins/permission.ts +7 -0
- package/src/plugins/push.ts +132 -0
- package/src/plugins/qr.ts +73 -0
- package/src/plugins/service.ts +47 -0
- package/src/runtime/app.ts +101 -0
- package/src/runtime/appEvents.ts +54 -0
- package/src/runtime/channel.ts +61 -0
- package/src/runtime/clipboard.ts +20 -0
- package/src/runtime/datetime.ts +329 -0
- package/src/runtime/device.ts +293 -0
- package/src/runtime/fetch.ts +77 -0
- package/src/runtime/files.ts +108 -0
- package/src/runtime/input.ts +175 -0
- package/src/runtime/media.ts +111 -0
- package/src/runtime/misc.ts +16 -0
- package/src/runtime/net.ts +36 -0
- package/src/runtime/rpc.ts +218 -0
- package/src/runtime/service.ts +83 -0
- package/src/runtime/share.ts +9 -0
- package/src/runtime/storage.ts +13 -0
- package/src/runtime/touch.ts +76 -0
- package/src/scene/defineScene.ts +1396 -0
- package/src/scene/editorPlugins.ts +110 -0
- package/src/scene/gizmos.ts +148 -0
- package/src/scene/grammar.ts +120 -0
- package/src/scene/level.ts +296 -0
- package/src/scene/material.ts +188 -0
- package/src/server/auth/appConfig.ts +12 -0
- package/src/server/auth/global.ts +80 -0
- package/src/server/auth/host.ts +318 -0
- package/src/server/auth/models.ts +83 -0
- package/src/server/auth/types.ts +50 -0
- package/src/server/channel.ts +56 -0
- package/src/server/context.ts +36 -0
- package/src/server/db/defineDb.ts +237 -0
- package/src/server/db/fields.ts +132 -0
- package/src/server/db/httpTransport.ts +93 -0
- package/src/server/db/index.ts +7 -0
- package/src/server/db/marci/query.ts +412 -0
- package/src/server/db/types.ts +202 -0
- package/src/server/errors.ts +12 -0
- package/src/server/host.ts +74 -0
- package/src/server/inject.ts +13 -0
- package/src/server/runtime.ts +133 -0
- package/src/server/validate.ts +87 -0
- package/src/ui/NativeView.ts +142 -0
- package/src/ui/UI.ts +39 -0
- package/src/ui/UIBottomSheet.ts +139 -0
- package/src/ui/UIButton.ts +101 -0
- package/src/ui/UIContainer.ts +60 -0
- package/src/ui/UIImage.ts +83 -0
- package/src/ui/UIInput.ts +185 -0
- package/src/ui/UIModal.ts +139 -0
- package/src/ui/UINode.ts +830 -0
- package/src/ui/UIPager.ts +362 -0
- package/src/ui/UIPopover.ts +100 -0
- package/src/ui/UIScreen.ts +123 -0
- package/src/ui/UIScrollable.ts +87 -0
- package/src/ui/UISpacer.ts +14 -0
- package/src/ui/UITabs.ts +236 -0
- package/src/ui/UIText.ts +51 -0
- package/src/ui/UIVideo.ts +88 -0
- package/src/ui/UIVirtualizedList.ts +241 -0
- package/src/ui/UIWidget.ts +127 -0
- package/src/ui/fonts.ts +13 -0
- package/src/ui/presentable.ts +117 -0
- package/src/ui/router.ts +132 -0
- package/src/ui/theme.ts +84 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `auth.model.user({...})` / `auth.model.session()` — the ONLY place auth touches the schema
|
|
3
|
+
* (docs/backend-plan.md §3.4 / decision 10). Both return ordinary `model(...)`s: the developer registers
|
|
4
|
+
* them in `defineDb({ User, Session, … })` under exactly those keys (other models relate to them with
|
|
5
|
+
* `t.one("User")` / `t.one("Session")`), and `defineDb` adds `Session` itself when it isn't declared —
|
|
6
|
+
* a device session exists from the first request, before any login, so every project db has one.
|
|
7
|
+
*
|
|
8
|
+
* Provider identity columns (`googleId`, `appleId`) follow `app.json` `auth.providers`, which the
|
|
9
|
+
* compiler hands the server bundle through ./appConfig.ts (a module it replaces at compile time).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { t, type Field } from "../db/fields"
|
|
13
|
+
import type { Fields, Model } from "../db/types"
|
|
14
|
+
import { app, type AppConfig, type AuthProvider } from "./appConfig"
|
|
15
|
+
|
|
16
|
+
export type { AppConfig, AuthProvider }
|
|
17
|
+
|
|
18
|
+
const seam = globalThis as unknown as { __lecodesApp?: AppConfig }
|
|
19
|
+
/** The project's `app.json` subset: the compiled-in module, else the test/tooling seam, else empty. */
|
|
20
|
+
export const appConfig = (): AppConfig => (Object.keys(app).length ? app : seam.__lecodesApp ?? {})
|
|
21
|
+
export const enabledProviders = (): AuthProvider[] => appConfig().auth?.providers ?? []
|
|
22
|
+
|
|
23
|
+
/** The model name the auth runtime binds to. */
|
|
24
|
+
export const AUTH_USER_MODEL = "User"
|
|
25
|
+
export const AUTH_SESSION_MODEL = "Session"
|
|
26
|
+
|
|
27
|
+
/** Marker on the two auth models (`model()` objects are otherwise plain). */
|
|
28
|
+
export type AuthModelKind = "user" | "session"
|
|
29
|
+
export type AuthModel<F extends Fields> = Model<F> & { readonly __auth: AuthModelKind }
|
|
30
|
+
|
|
31
|
+
// ───────────────────────────── field sets ─────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Built-in `User` fields (what `auth.user` is typed with). */
|
|
34
|
+
export const userFields = () => ({
|
|
35
|
+
email: t.string().optional().unique(),
|
|
36
|
+
name: t.string().optional(),
|
|
37
|
+
avatar: t.string().optional(),
|
|
38
|
+
/** Plain string column, `"user"` by default — `auth.requireUser({ role: "admin" })` is field equality. */
|
|
39
|
+
role: t.string().default("user"),
|
|
40
|
+
createdAt: t.date().default("now"),
|
|
41
|
+
})
|
|
42
|
+
export type UserFields = ReturnType<typeof userFields>
|
|
43
|
+
|
|
44
|
+
/** `<provider>Id` identity columns for the OAuth providers enabled in app.json. */
|
|
45
|
+
const providerFields = (): Record<string, Field<any, any>> => {
|
|
46
|
+
const out: Record<string, Field<any, any>> = {}
|
|
47
|
+
for (const p of enabledProviders()) if (p !== "email") out[`${p}Id`] = t.string().optional().unique()
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Built-in `Session` fields. `user` is added by `defineDb` when a `User` model exists (schema-time, so a
|
|
52
|
+
* db without an auth user model has sessions without a user column). */
|
|
53
|
+
export const sessionFields = () => ({
|
|
54
|
+
/** sha256 of the bearer token the device holds — the token itself is never stored. */
|
|
55
|
+
tokenHash: t.string().unique(),
|
|
56
|
+
createdAt: t.date().default("now"),
|
|
57
|
+
lastSeenAt: t.date().default("now"),
|
|
58
|
+
revoked: t.bool().default(false),
|
|
59
|
+
// email-code sign-in state lives on the requesting session (no code table)
|
|
60
|
+
pendingEmail: t.string().optional(),
|
|
61
|
+
codeHash: t.string().optional(),
|
|
62
|
+
codeExpires: t.date().optional(),
|
|
63
|
+
codeAttempts: t.int().default(0),
|
|
64
|
+
})
|
|
65
|
+
export type SessionFields = ReturnType<typeof sessionFields> & { user: ReturnType<typeof sessionUserField> }
|
|
66
|
+
export const sessionUserField = () => t.one(AUTH_USER_MODEL).optional().onDelete("cascade")
|
|
67
|
+
|
|
68
|
+
// ───────────────────────────── factories ─────────────────────────────
|
|
69
|
+
|
|
70
|
+
// Built inline (not via defineDb's `model()`) to keep this module import-free of defineDb, which imports us.
|
|
71
|
+
const authModel = <F extends Fields>(kind: AuthModelKind, fields: F): AuthModel<F> =>
|
|
72
|
+
({ fields, __model: true, __auth: kind }) as AuthModel<F>
|
|
73
|
+
|
|
74
|
+
/** `auth.model.user({...extra})` — the built-in user (+ provider ids from app.json) plus the developer's fields. */
|
|
75
|
+
export const authUserModel = <E extends Fields = {}>(extra?: E): AuthModel<UserFields & E> =>
|
|
76
|
+
authModel("user", { ...userFields(), ...providerFields(), ...(extra ?? {}) } as UserFields & E)
|
|
77
|
+
|
|
78
|
+
/** `auth.model.session({...extra})` — declare it only to relate to sessions (`t.one("Session")`) or to add fields. */
|
|
79
|
+
export const authSessionModel = <E extends Fields = {}>(extra?: E): AuthModel<SessionFields & E> =>
|
|
80
|
+
authModel("session", { ...sessionFields(), ...(extra ?? {}) } as unknown as SessionFields & E)
|
|
81
|
+
|
|
82
|
+
export const isAuthModel = (m: unknown, kind?: AuthModelKind): m is AuthModel<any> =>
|
|
83
|
+
typeof m === "object" && m !== null && "__auth" in m && (kind === undefined || (m as AuthModel<any>).__auth === kind)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shapes of the auth layer (docs/backend-plan.md §3.4). Two sides meet through them:
|
|
3
|
+
* - the BUNDLE side (`auth` global, ./global.ts) — what `*.server.ts` code calls;
|
|
4
|
+
* - the HOST side (./host.ts) — sessions, email codes and OAuth binding, run by the runner with the
|
|
5
|
+
* project's db handle and the platform's secrets. Installed on `globalThis.__lecodesAuth`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { RequestContext } from "../context"
|
|
9
|
+
import type { AuthProvider } from "./models"
|
|
10
|
+
|
|
11
|
+
/** The built-in user row — what `auth.user` is typed with (custom fields exist at runtime; select them via `db.user`). */
|
|
12
|
+
export type AuthUser = {
|
|
13
|
+
id: number
|
|
14
|
+
email: string | null
|
|
15
|
+
name: string | null
|
|
16
|
+
avatar: string | null
|
|
17
|
+
role: string
|
|
18
|
+
createdAt: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Per-request auth state, resolved by the host before the endpoint runs (`RequestContext.auth`). */
|
|
22
|
+
export type AuthState = {
|
|
23
|
+
sessionId: number
|
|
24
|
+
userId: number | null
|
|
25
|
+
user: AuthUser | null
|
|
26
|
+
/** A token to hand back to the client in this response (a new guest session, or a rotation). */
|
|
27
|
+
issuedToken?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What an OAuth provider hands the runner after a successful web sign-in. */
|
|
31
|
+
export type OAuthProfile = { sub: string, email?: string | null, emailVerified?: boolean, name?: string | null, avatar?: string | null }
|
|
32
|
+
/** A credential a NATIVE provider SDK produced in the app (client plugin `OAuth.signIn`): an access token
|
|
33
|
+
* (Yandex, VK ID) or an OpenID identity token (Google, Apple). Verified server-side, never trusted as-is. */
|
|
34
|
+
export type OAuthCredential = { accessToken?: string, idToken?: string, name?: string | null, email?: string | null }
|
|
35
|
+
|
|
36
|
+
/** Host operations the bundle's `auth` global forwards to (all request-scoped through `ctx`). */
|
|
37
|
+
export type AuthOps = {
|
|
38
|
+
email: {
|
|
39
|
+
sendCode(ctx: RequestContext, email: string): Promise<void>
|
|
40
|
+
verify(ctx: RequestContext, email: string, code: string): Promise<AuthUser>
|
|
41
|
+
}
|
|
42
|
+
oauth: {
|
|
43
|
+
url(ctx: RequestContext, provider: Exclude<AuthProvider, "email">): Promise<string>
|
|
44
|
+
signIn(ctx: RequestContext, provider: Exclude<AuthProvider, "email">, credential: OAuthCredential): Promise<AuthUser>
|
|
45
|
+
}
|
|
46
|
+
signIn(ctx: RequestContext, userId: number): Promise<AuthUser>
|
|
47
|
+
signOut(ctx: RequestContext): Promise<void>
|
|
48
|
+
/** `db.session` filtered to the current user's sessions (or just this device's, when signed out). */
|
|
49
|
+
sessions(ctx: RequestContext): any
|
|
50
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server side of channels (docs/backend-plan.md §3.3): typed pub/sub, server → client only.
|
|
3
|
+
*
|
|
4
|
+
* type ChatEvents = { message: { from: string; text: string }; typing: { from: string } }
|
|
5
|
+
* export const chat = channel<ChatEvents>({ onJoin: room => { auth.requireUser() } })
|
|
6
|
+
* chat.publish(room, "message", { from, text })
|
|
7
|
+
*
|
|
8
|
+
* A channel's identity is its export (`<path>#<name>`), assigned by the runtime when the bundle is
|
|
9
|
+
* loaded (`loadServerModules`); `publish` before that is an error. Delivery goes through the
|
|
10
|
+
* `globalThis.__lecodesPublish` seam the runner installs (same reason as ./context.ts: the bundle
|
|
11
|
+
* carries its own SDK copy).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ChannelEvents = Record<string, unknown>
|
|
15
|
+
|
|
16
|
+
export type ChannelOptions = {
|
|
17
|
+
/** Called when a client subscribes to `topic`; throw (e.g. `ApiError(403)`) to refuse. */
|
|
18
|
+
onJoin?: (topic: string) => void | Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Channel<E extends ChannelEvents> {
|
|
22
|
+
/** @internal set by the runtime: `<path>#<export>` */
|
|
23
|
+
__id: string | null
|
|
24
|
+
/** @internal */
|
|
25
|
+
readonly __channel: true
|
|
26
|
+
readonly options: ChannelOptions
|
|
27
|
+
/** Deliver `event` with `payload` to every subscriber of `topic` (at-most-once). */
|
|
28
|
+
publish<K extends keyof E & string>(topic: string, event: K, payload: E[K]): void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Publisher = (channelId: string, topic: string, event: string, payload: unknown) => void
|
|
32
|
+
const seam = globalThis as unknown as { __lecodesPublish?: Publisher }
|
|
33
|
+
|
|
34
|
+
/** Host hook (runner / test harness): where `publish` delivers to. */
|
|
35
|
+
export const setChannelPublisher = (publisher: Publisher | null) => {
|
|
36
|
+
if (publisher) seam.__lecodesPublish = publisher
|
|
37
|
+
else delete seam.__lecodesPublish
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const channel = <E extends ChannelEvents>(options: ChannelOptions = {}): Channel<E> => {
|
|
41
|
+
const ch: Channel<E> = {
|
|
42
|
+
__id: null,
|
|
43
|
+
__channel: true,
|
|
44
|
+
options,
|
|
45
|
+
publish(topic, event, payload) {
|
|
46
|
+
if (!ch.__id) throw new Error("channel.publish: the channel is not registered — export it from a *.server.ts file")
|
|
47
|
+
const publish = seam.__lecodesPublish
|
|
48
|
+
if (!publish) throw new Error("channel.publish: no host publisher installed")
|
|
49
|
+
publish(ch.__id, topic, event, payload)
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
return ch
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const isChannel = (v: unknown): v is Channel<any> =>
|
|
56
|
+
typeof v === "object" && v !== null && (v as Channel<any>).__channel === true
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request context for server code. The runner keeps the current request in an AsyncLocalStorage
|
|
3
|
+
* and exposes it through `globalThis.__lecodesRequest` — a *global* seam on purpose: the compiled
|
|
4
|
+
* server bundle carries its own copy of this module (chisel bundles the SDK in), so the host can't
|
|
5
|
+
* hand it a value by import. `request` reads through that seam; outside a request (module load,
|
|
6
|
+
* timers after the response) every member is undefined.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type RequestContext = {
|
|
10
|
+
/** Endpoint id `<path>#<export>` (or the channel id for `onJoin`). */
|
|
11
|
+
id: string
|
|
12
|
+
headers: Record<string, string>
|
|
13
|
+
ip?: string
|
|
14
|
+
/** Bearer session token as sent by the client transport (raw; auth resolves it). */
|
|
15
|
+
sessionToken?: string
|
|
16
|
+
/** Set by the auth runtime after resolving the token — read via `auth.*`, not here. */
|
|
17
|
+
auth?: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Provider = () => RequestContext | undefined
|
|
21
|
+
const seam = globalThis as unknown as { __lecodesRequest?: Provider }
|
|
22
|
+
|
|
23
|
+
/** Host hook (runner / test harness): install the current-request provider. */
|
|
24
|
+
export const setRequestProvider = (provider: Provider | null) => {
|
|
25
|
+
if (provider) seam.__lecodesRequest = provider
|
|
26
|
+
else delete seam.__lecodesRequest
|
|
27
|
+
}
|
|
28
|
+
/** @internal */
|
|
29
|
+
export const currentRequest = (): RequestContext | undefined => seam.__lecodesRequest?.()
|
|
30
|
+
|
|
31
|
+
/** The current request (server global). Undefined members outside a request. */
|
|
32
|
+
export const request = {
|
|
33
|
+
get id(): string | undefined { return currentRequest()?.id },
|
|
34
|
+
get headers(): Record<string, string> { return currentRequest()?.headers ?? {} },
|
|
35
|
+
get ip(): string | undefined { return currentRequest()?.ip },
|
|
36
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `model({...})` + `defineDb({ User, Post })` — the schema registry and the typed db handle.
|
|
3
|
+
*
|
|
4
|
+
* Runtime responsibilities: validate relation names, resolve `t.many()` back-fields, emit the `.marci`
|
|
5
|
+
* schema text (`db.$schema`), and turn collection calls into lazy `Op`s over a `MarciTransport`
|
|
6
|
+
* (`marcidb-embedded` handle, HTTP client, or a test fake). The transport is either passed to
|
|
7
|
+
* `defineDb(models, { transport })` or provided by the host through `setDbTransport()` — user code
|
|
8
|
+
* never sees a connection string.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Field, FieldDef, ScalarKind } from "./fields"
|
|
12
|
+
import { AUTH_SESSION_MODEL, AUTH_USER_MODEL, authSessionModel, isAuthModel, sessionUserField } from "../auth/models"
|
|
13
|
+
import { createQueryLayer, type FieldDesc, type MarciOp, type MarciTransport, type ModelsMeta } from "./marci/query"
|
|
14
|
+
import type { AuthBinding, Db, Fields, Model, Schema, ValidateRefs } from "./types"
|
|
15
|
+
|
|
16
|
+
export const model = <F extends Fields>(fields: F): Model<F> => ({ fields, __model: true })
|
|
17
|
+
|
|
18
|
+
// ───────────────────────────── transport ─────────────────────────────
|
|
19
|
+
|
|
20
|
+
// A GLOBAL seam (not a module variable): the compiled server bundle carries its own copy of this
|
|
21
|
+
// module, so the host's `setDbTransport` and the bundle's `defineDb` only meet through globalThis.
|
|
22
|
+
const seam = globalThis as unknown as { __lecodesDbTransport?: MarciTransport, __lecodesDbs?: Db<any>[] }
|
|
23
|
+
/** Host hook: the runner/dev-server installs the project's transport before loading user code. */
|
|
24
|
+
export const setDbTransport = (transport: MarciTransport | null) => {
|
|
25
|
+
if (transport) seam.__lecodesDbTransport = transport
|
|
26
|
+
else delete seam.__lecodesDbTransport
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Every `defineDb` call registers its handle here so the host finds the schema whether or not the
|
|
30
|
+
* project exports `db` (`const db = defineDb(...)` used only inside endpoints is the common case).
|
|
31
|
+
* The host reads and clears the list after evaluating a bundle (`loadServerModules`).
|
|
32
|
+
*/
|
|
33
|
+
export const takeRegisteredDbs = (): Db<any>[] => {
|
|
34
|
+
const dbs = seam.__lecodesDbs ?? []
|
|
35
|
+
seam.__lecodesDbs = []
|
|
36
|
+
return dbs
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const opOf = (o: unknown): MarciOp => {
|
|
40
|
+
const op = (o as { __op?: MarciOp })?.__op
|
|
41
|
+
if (!op) throw new Error("db.$transaction: every item must be a db.<model>.<method>(...) call")
|
|
42
|
+
return op
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A reference to a previous operation's result inside `$transaction` (resolved server-side): `ref("0.id")`. */
|
|
46
|
+
export const ref = (path: string): any => ({ $ref: path })
|
|
47
|
+
|
|
48
|
+
// ───────────────────────────── metadata ─────────────────────────────
|
|
49
|
+
|
|
50
|
+
type ModelMeta = {
|
|
51
|
+
name: string
|
|
52
|
+
fields: Record<string, FieldDef>
|
|
53
|
+
idKeys: string[]
|
|
54
|
+
/** id is a single uuid key we generate on insert. */
|
|
55
|
+
uuidId: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isRel = (d: FieldDef) => d.kind === "one" || d.kind === "many" || d.kind === "list"
|
|
59
|
+
|
|
60
|
+
const buildMeta = (models: Record<string, Model>): Record<string, ModelMeta> => {
|
|
61
|
+
const metas: Record<string, ModelMeta> = {}
|
|
62
|
+
for (const [name, m] of Object.entries(models)) {
|
|
63
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) throw new Error(`defineDb: model name "${name}" must be PascalCase`)
|
|
64
|
+
const fields: Record<string, FieldDef> = {}
|
|
65
|
+
for (const [key, f] of Object.entries(m.fields as Fields)) fields[key] = { ...(f as Field<any, any>).def }
|
|
66
|
+
if (fields.id && !fields.id.isId) fields.id.isId = true
|
|
67
|
+
const idKeys = Object.keys(fields).filter(k => fields[k].isId)
|
|
68
|
+
const uuidId = idKeys.length === 1 && idKeys[0] === "id" && fields.id.scalar === "uuid"
|
|
69
|
+
metas[name] = { name, fields, idKeys, uuidId }
|
|
70
|
+
}
|
|
71
|
+
// relations: targets must exist; `many` needs a back field
|
|
72
|
+
for (const meta of Object.values(metas)) {
|
|
73
|
+
for (const [key, d] of Object.entries(meta.fields)) {
|
|
74
|
+
if (!isRel(d)) continue
|
|
75
|
+
const target = metas[d.model!]
|
|
76
|
+
if (!target) throw new Error(`defineDb: ${meta.name}.${key} references unknown model "${d.model}"`)
|
|
77
|
+
if (d.kind !== "many") continue
|
|
78
|
+
if (d.back) {
|
|
79
|
+
const back = target.fields[d.back]
|
|
80
|
+
if (!back || back.kind !== "one" || back.model !== meta.name)
|
|
81
|
+
throw new Error(`defineDb: ${meta.name}.${key}: "${d.model}.${d.back}" is not a t.one("${meta.name}") field`)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
const candidates = Object.entries(target.fields).filter(([, f]) => f.kind === "one" && f.model === meta.name).map(([k]) => k)
|
|
85
|
+
if (candidates.length !== 1)
|
|
86
|
+
throw new Error(
|
|
87
|
+
`defineDb: ${meta.name}.${key}: cannot infer the back field on ${d.model} ` +
|
|
88
|
+
(candidates.length ? `(candidates: ${candidates.join(", ")}) — pass it: t.many("${d.model}", "<field>")`
|
|
89
|
+
: `— add a t.one("${meta.name}") field to ${d.model}`))
|
|
90
|
+
d.back = candidates[0]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return metas
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ───────────────────────────── .marci emitter ─────────────────────────────
|
|
97
|
+
|
|
98
|
+
const SCALAR: Record<Exclude<ScalarKind, "">, string> = {
|
|
99
|
+
string: "String", int: "Int", float: "Double", bool: "Bool", date: "DateTime", uuid: "Byte[16]",
|
|
100
|
+
}
|
|
101
|
+
const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
|
|
102
|
+
const ON_DELETE = { cascade: "Cascade", setNull: "SetNull", restrict: "Restrict" } as const
|
|
103
|
+
|
|
104
|
+
const literal = (v: unknown, d: FieldDef): string => {
|
|
105
|
+
if (d.scalar === "date" && v === "now") return "now()"
|
|
106
|
+
if (Array.isArray(v)) return `[${v.map(x => literal(x, { ...d, array: false })).join(", ")}]`
|
|
107
|
+
if (typeof v === "string") return d.kind === "enum" ? v : JSON.stringify(v)
|
|
108
|
+
return String(v)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const emitField = (owner: string, key: string, d: FieldDef, extra: string[]): string => {
|
|
112
|
+
let type: string
|
|
113
|
+
const attrs: string[] = []
|
|
114
|
+
switch (d.kind) {
|
|
115
|
+
case "scalar":
|
|
116
|
+
type = d.vector ? `Float[${d.vector.size}]` : SCALAR[d.scalar as Exclude<ScalarKind, "">]
|
|
117
|
+
if (d.scalar === "uuid") attrs.push("@format(uuid)")
|
|
118
|
+
break
|
|
119
|
+
case "json": type = "Json"; break
|
|
120
|
+
case "enum": {
|
|
121
|
+
const name = `${owner}${cap(key)}`
|
|
122
|
+
extra.push(`enum ${name} {\n${d.values!.map(v => ` ${v}`).join("\n")}\n}`)
|
|
123
|
+
type = name; break
|
|
124
|
+
}
|
|
125
|
+
case "struct": {
|
|
126
|
+
const name = `${owner}${cap(key)}`
|
|
127
|
+
const inner: string[] = []
|
|
128
|
+
for (const [k, f] of Object.entries(d.fields!)) inner.push(emitField(name, k, (f as Field<any, any>).def, extra))
|
|
129
|
+
extra.push(`struct ${name} {\n${inner.join("\n")}\n}`)
|
|
130
|
+
type = name; break
|
|
131
|
+
}
|
|
132
|
+
case "one": type = d.model!; break
|
|
133
|
+
case "many": type = `${d.model}[]`; attrs.push(`@bind(${d.model}.${d.back})`); break
|
|
134
|
+
case "list": type = `${d.model}[]`; attrs.push("@list"); break
|
|
135
|
+
}
|
|
136
|
+
if (d.array && d.kind !== "many" && d.kind !== "list") type += "[]"
|
|
137
|
+
if (d.optional) type += "?"
|
|
138
|
+
if (d.isId) attrs.unshift("@id")
|
|
139
|
+
if (d.unique) attrs.push("@unique")
|
|
140
|
+
if (d.index) attrs.push("@index")
|
|
141
|
+
if (d.hasDefault) attrs.push(`@default(${literal(d.default, d)})`)
|
|
142
|
+
if (d.onDelete) attrs.push(`@onDelete(${ON_DELETE[d.onDelete]})`)
|
|
143
|
+
if (d.fulltext) attrs.push(`@fulltext(${d.fulltext})`)
|
|
144
|
+
if (d.vector) attrs.push(`@vector(${d.vector.metric})`)
|
|
145
|
+
return ` ${key.padEnd(12)} ${type}${attrs.length ? " " + attrs.join(" ") : ""}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const toMarci = (metas: Record<string, ModelMeta>): string => {
|
|
149
|
+
const out: string[] = []
|
|
150
|
+
const extra: string[] = []
|
|
151
|
+
for (const meta of Object.values(metas)) {
|
|
152
|
+
const lines = Object.entries(meta.fields).map(([k, d]) => emitField(meta.name, k, d, extra))
|
|
153
|
+
out.push(`model ${meta.name} {\n${lines.join("\n")}\n}`)
|
|
154
|
+
}
|
|
155
|
+
return [...out, ...extra].join("\n\n") + "\n"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ───────────────────────────── queries ─────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The model metadata marcidb's query layer reads (`MODELS` in a generated client): field descriptors per
|
|
162
|
+
* model — `key` / `body` scalars, `one` / `many` relations with their target. Structs are pseudo-models
|
|
163
|
+
* (`User.info`) so an empty nested select fills with the struct's own scalars, as marcidb does.
|
|
164
|
+
*/
|
|
165
|
+
const modelsMeta = (metas: Record<string, ModelMeta>): ModelsMeta => {
|
|
166
|
+
const out: Record<string, FieldDesc[]> = {}
|
|
167
|
+
const describe = (name: string, fields: Record<string, FieldDef>, implicitId: boolean) => {
|
|
168
|
+
const descs: FieldDesc[] = implicitId ? [{ n: "id", k: "key" }] : []
|
|
169
|
+
for (const [key, d] of Object.entries(fields)) {
|
|
170
|
+
if (isRel(d)) descs.push({ n: key, k: d.kind === "one" ? "one" : "many", m: d.model! })
|
|
171
|
+
else if (d.kind === "struct") {
|
|
172
|
+
const sub = `${name}.${key}`
|
|
173
|
+
const inner: Record<string, FieldDef> = {}
|
|
174
|
+
for (const [ik, f] of Object.entries(d.fields!)) inner[ik] = (f as Field<any, any>).def
|
|
175
|
+
describe(sub, inner, false)
|
|
176
|
+
descs.push({ n: key, k: "one", m: sub })
|
|
177
|
+
}
|
|
178
|
+
else descs.push({ n: key, k: d.isId ? "key" : "body" })
|
|
179
|
+
}
|
|
180
|
+
out[name] = descs
|
|
181
|
+
}
|
|
182
|
+
for (const meta of Object.values(metas)) describe(meta.name, meta.fields, meta.idKeys.length === 0)
|
|
183
|
+
return out
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const uncap = (s: string) => s.charAt(0).toLowerCase() + s.slice(1)
|
|
187
|
+
|
|
188
|
+
export type DefineDbOptions = { transport?: MarciTransport }
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The schema registry: model name = key (`db.user` ← `User`). Relation strings are checked against the
|
|
192
|
+
* keys both at the type level (`ValidateRefs`) and at runtime.
|
|
193
|
+
*/
|
|
194
|
+
/**
|
|
195
|
+
* The auth models (docs/backend-plan.md §3.4): `auth.model.user()` must be registered as `User`,
|
|
196
|
+
* `auth.model.session()` as `Session`; `Session` is added when the project doesn't declare it (a device
|
|
197
|
+
* session exists before any login), and gets its `user` column exactly when a `User` model exists.
|
|
198
|
+
*/
|
|
199
|
+
const withAuthModels = (models: Record<string, Model>): { models: Record<string, Model>, auth: AuthBinding } => {
|
|
200
|
+
for (const [name, m] of Object.entries(models)) {
|
|
201
|
+
if (isAuthModel(m, "user") && name !== AUTH_USER_MODEL) throw new Error(`defineDb: auth.model.user() must be registered as "${AUTH_USER_MODEL}" (got "${name}")`)
|
|
202
|
+
if (isAuthModel(m, "session") && name !== AUTH_SESSION_MODEL) throw new Error(`defineDb: auth.model.session() must be registered as "${AUTH_SESSION_MODEL}" (got "${name}")`)
|
|
203
|
+
}
|
|
204
|
+
// A plain `model()` named User is just a model (no auth binding); only auth.model.user() binds.
|
|
205
|
+
const user = isAuthModel(models[AUTH_USER_MODEL], "user") ? models[AUTH_USER_MODEL] : null
|
|
206
|
+
let session = models[AUTH_SESSION_MODEL]
|
|
207
|
+
if (session && !isAuthModel(session, "session")) throw new Error(`defineDb: "${AUTH_SESSION_MODEL}" is reserved for auth.model.session()`)
|
|
208
|
+
if (!session) session = authSessionModel()
|
|
209
|
+
if (user && !("user" in session.fields)) session = { ...session, fields: { ...session.fields, user: sessionUserField() } }
|
|
210
|
+
return { models: { ...models, [AUTH_SESSION_MODEL]: session }, auth: { user: user ? AUTH_USER_MODEL : null, session: AUTH_SESSION_MODEL } }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const defineDb = <const S extends Schema>(declared: S & ValidateRefs<S>, options: DefineDbOptions = {}): Db<S> => {
|
|
214
|
+
const { models, auth } = withAuthModels(declared as Record<string, Model>)
|
|
215
|
+
const metas = buildMeta(models)
|
|
216
|
+
const transport = () => {
|
|
217
|
+
const t = options.transport ?? seam.__lecodesDbTransport
|
|
218
|
+
if (!t) throw new Error("db: no transport — the host must call setDbTransport() before user code runs")
|
|
219
|
+
return t
|
|
220
|
+
}
|
|
221
|
+
// marcidb's own query builder over our metadata; the transport is the only host-specific piece
|
|
222
|
+
const { collection } = createQueryLayer({
|
|
223
|
+
models: modelsMeta(metas),
|
|
224
|
+
run: op => transport().exec(op),
|
|
225
|
+
// a single uuid key is generated client-side so `insert` needs no id (the .marci says @format(uuid))
|
|
226
|
+
prepareInsert: (name, data) => (metas[name].uuidId && data?.id == null ? { ...data, id: crypto.randomUUID() } : data),
|
|
227
|
+
})
|
|
228
|
+
const db: Record<string, unknown> = {
|
|
229
|
+
$schema: toMarci(metas),
|
|
230
|
+
$models: models,
|
|
231
|
+
$auth: auth,
|
|
232
|
+
$transaction: async (ops: unknown[]) => transport().batch(ops.map(opOf)),
|
|
233
|
+
}
|
|
234
|
+
for (const name of Object.keys(metas)) db[uncap(name)] = collection(name)
|
|
235
|
+
;(seam.__lecodesDbs ??= []).push(db as Db<any>)
|
|
236
|
+
return db as Db<S>
|
|
237
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `t` — field builders for `model({...})`. Each builder returns an immutable `Field` carrying a runtime
|
|
3
|
+
* `FieldDef` (what `.marci` is emitted from) and two phantom type parameters: the TS value type and a
|
|
4
|
+
* `FieldMeta` record (kind / optional / default / …) that `defineDb`'s type layer projects into row,
|
|
5
|
+
* insert, update, select and where types. Chained modifiers return a NEW field (builders are never
|
|
6
|
+
* mutated), so `const s = t.string(); { a: s, b: s.optional() }` behaves as expected.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { JsonValue } from "./marci/query"
|
|
10
|
+
|
|
11
|
+
export type ScalarKind = "string" | "int" | "float" | "bool" | "date" | "uuid" | ""
|
|
12
|
+
export type FieldKind = "scalar" | "json" | "enum" | "struct" | "one" | "many" | "list"
|
|
13
|
+
export type OnDelete = "cascade" | "setNull" | "restrict"
|
|
14
|
+
|
|
15
|
+
/** Phantom metadata — the type-level twin of `FieldDef`. */
|
|
16
|
+
export type FieldMeta = {
|
|
17
|
+
kind: FieldKind
|
|
18
|
+
scalar: ScalarKind
|
|
19
|
+
optional: boolean
|
|
20
|
+
hasDefault: boolean
|
|
21
|
+
array: boolean
|
|
22
|
+
isId: boolean
|
|
23
|
+
/** Related model name for one/many/list; "" otherwise. */
|
|
24
|
+
model: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type DefaultMeta = { kind: "scalar", scalar: "", optional: false, hasDefault: false, array: false, isId: false, model: "" }
|
|
28
|
+
export type Meta<O extends Partial<FieldMeta>> = { [P in keyof FieldMeta]: P extends keyof O ? O[P] : DefaultMeta[P] }
|
|
29
|
+
type SetMeta<M extends FieldMeta, K extends keyof FieldMeta, V> = { [P in keyof FieldMeta]: P extends K ? V : M[P] }
|
|
30
|
+
|
|
31
|
+
/** Runtime field definition — everything the `.marci` emitter and the runtime need. */
|
|
32
|
+
export type FieldDef = {
|
|
33
|
+
kind: FieldKind
|
|
34
|
+
scalar?: ScalarKind
|
|
35
|
+
optional: boolean
|
|
36
|
+
unique: boolean
|
|
37
|
+
index: boolean
|
|
38
|
+
hasDefault: boolean
|
|
39
|
+
default?: unknown
|
|
40
|
+
array: boolean
|
|
41
|
+
isId: boolean
|
|
42
|
+
onDelete?: OnDelete
|
|
43
|
+
fulltext?: "multi" | "english" | "russian"
|
|
44
|
+
vector?: { size: number, metric: "cosine" | "euclidean" }
|
|
45
|
+
/** enum */
|
|
46
|
+
values?: readonly string[]
|
|
47
|
+
/** struct */
|
|
48
|
+
fields?: Record<string, Field<any, any>>
|
|
49
|
+
/** one / many / list */
|
|
50
|
+
model?: string
|
|
51
|
+
/** many: the field on the related model that holds the reference (`@bind`); resolved by defineDb when omitted. */
|
|
52
|
+
back?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type DefaultOf<T, M extends FieldMeta> =
|
|
56
|
+
M["scalar"] extends "date" ? "now" | number : M["array"] extends true ? T[] : T
|
|
57
|
+
|
|
58
|
+
export interface Field<T, M extends FieldMeta = FieldMeta> {
|
|
59
|
+
/** phantom (type-level only — never set at runtime) */ readonly __t?: T
|
|
60
|
+
/** phantom (type-level only — never set at runtime) */ readonly __m?: M
|
|
61
|
+
readonly def: FieldDef
|
|
62
|
+
/** Nullable column (`String?`). */
|
|
63
|
+
optional(): Field<T, SetMeta<M, "optional", true>>
|
|
64
|
+
/** `@unique` (on a relation: one-to-one). */
|
|
65
|
+
unique(): Field<T, M>
|
|
66
|
+
/** `@index` — used by `$where`, `$order`, aggregations. */
|
|
67
|
+
index(): Field<T, M>
|
|
68
|
+
/** `@default(...)`; `"now"` on `t.date()` → `now()`. Makes the field optional on insert. */
|
|
69
|
+
default(value: DefaultOf<T, M>): Field<T, SetMeta<M, "hasDefault", true>>
|
|
70
|
+
/** Primitive list (`String[]`). */
|
|
71
|
+
array(): Field<T, SetMeta<M, "array", true>>
|
|
72
|
+
/** `@id` — primary key (several → composite). A field literally named `id` is the key implicitly. */
|
|
73
|
+
id(): Field<T, SetMeta<M, "isId", true>>
|
|
74
|
+
/** Referential policy for `t.one()` (defaults: required → restrict, optional → setNull). */
|
|
75
|
+
onDelete(policy: OnDelete): Field<T, M>
|
|
76
|
+
/** `@fulltext(...)` module index on a string field; query with `$search`. */
|
|
77
|
+
fulltext(lang?: "multi" | "english" | "russian"): Field<T, M>
|
|
78
|
+
/** `Float[n] @vector(metric)` — nearest-neighbour index; query with `$near`. */
|
|
79
|
+
vector(size: number, metric?: "cosine" | "euclidean"): Field<T, M>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const create = <T, M extends FieldMeta>(def: FieldDef): Field<T, M> => {
|
|
83
|
+
const next = (patch: Partial<FieldDef>) => create<any, any>({ ...def, ...patch })
|
|
84
|
+
return {
|
|
85
|
+
def,
|
|
86
|
+
optional: () => next({ optional: true }),
|
|
87
|
+
unique: () => next({ unique: true }),
|
|
88
|
+
index: () => next({ index: true }),
|
|
89
|
+
default: (value: unknown) => next({ hasDefault: true, default: value }),
|
|
90
|
+
array: () => next({ array: true }),
|
|
91
|
+
id: () => next({ isId: true }),
|
|
92
|
+
onDelete: (policy) => next({ onDelete: policy }),
|
|
93
|
+
fulltext: (lang = "multi") => next({ fulltext: lang }),
|
|
94
|
+
vector: (size, metric = "cosine") => next({ vector: { size, metric } }),
|
|
95
|
+
} as Field<T, M>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const base = (kind: FieldKind, extra: Partial<FieldDef> = {}): FieldDef => ({
|
|
99
|
+
kind, optional: false, unique: false, index: false, hasDefault: false, array: false, isId: false, ...extra,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const scalar = <T, K extends ScalarKind>(scalar: K) =>
|
|
103
|
+
create<T, Meta<{ kind: "scalar", scalar: K }>>(base("scalar", { scalar }))
|
|
104
|
+
|
|
105
|
+
export const t = {
|
|
106
|
+
string: () => scalar<string, "string">("string"),
|
|
107
|
+
/** 64-bit signed integer. */
|
|
108
|
+
int: () => scalar<number, "int">("int"),
|
|
109
|
+
/** 64-bit float. */
|
|
110
|
+
float: () => scalar<number, "float">("float"),
|
|
111
|
+
bool: () => scalar<boolean, "bool">("bool"),
|
|
112
|
+
/** Epoch milliseconds — reads and writes as `number`. */
|
|
113
|
+
date: () => scalar<number, "date">("date"),
|
|
114
|
+
/** UUID string (16 bytes in storage). On an `id` field the runtime generates it on insert. */
|
|
115
|
+
uuid: () => scalar<string, "uuid">("uuid"),
|
|
116
|
+
/** Any JSON value; `J` is TS-only (marcidb stores it schemaless). Filter by dot-path in `$where`. */
|
|
117
|
+
json: <J = JsonValue>() => create<J, Meta<{ kind: "json" }>>(base("json")),
|
|
118
|
+
/** String enum: `t.enum("draft", "published")`. */
|
|
119
|
+
enum: <const V extends readonly [string, ...string[]]>(...values: V) =>
|
|
120
|
+
create<V[number], Meta<{ kind: "enum" }>>(base("enum", { values })),
|
|
121
|
+
/** Owned nested object (marcidb `struct`): created/updated with the parent, no id of its own. */
|
|
122
|
+
struct: <F extends Record<string, Field<any, any>>>(fields: F) =>
|
|
123
|
+
create<F, Meta<{ kind: "struct" }>>(base("struct", { fields })),
|
|
124
|
+
/** Reference to one row of `model` (by id). Required unless `.optional()`. */
|
|
125
|
+
one: <N extends string>(model: N) => create<never, Meta<{ kind: "one", model: N }>>(base("one", { model })),
|
|
126
|
+
/** Reverse side of a `t.one()` on `model` (the field is inferred when unambiguous). Read-only list. */
|
|
127
|
+
many: <N extends string>(model: N, backField?: string) =>
|
|
128
|
+
create<never, Meta<{ kind: "many", model: N }>>(base("many", { model, back: backField })),
|
|
129
|
+
/** Ordered relation list stored inline (`@list`) — hand-arranged collections, duplicates allowed. */
|
|
130
|
+
list: <N extends string>(model: N) => create<never, Meta<{ kind: "list", model: N }>>(base("list", { model })),
|
|
131
|
+
}
|
|
132
|
+
export type T = typeof t
|