lecodes-sdk 2.0.7 → 2.0.8
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/dist/global.d.ts +4 -0
- package/dist/types/inject.d.ts +1 -0
- package/dist/types/runtime/rpc.d.ts +2 -0
- package/dist/types/server/context.d.ts +4 -0
- package/dist/types/server/db/defineDb.d.ts +1 -1
- package/dist/types/server/db/fields.d.ts +25 -1
- package/dist/types/server/db/marci/query.d.ts +39 -2
- package/dist/types/server/db/types.d.ts +13 -7
- package/dist/types/server/files/db.d.ts +39 -0
- package/dist/types/server/files/models.d.ts +131 -0
- package/dist/types/server/inject.d.ts +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +3 -2
- package/src/compile/libraryImports.ts +9 -0
- package/src/compile/serverTypes.ts +6 -0
- package/src/inject.ts +1 -0
- package/src/runtime/rpc.ts +15 -3
- package/src/server/context.ts +4 -0
- package/src/server/db/defineDb.ts +62 -9
- package/src/server/db/fields.ts +21 -1
- package/src/server/db/httpTransport.ts +9 -1
- package/src/server/db/marci/query.ts +71 -4
- package/src/server/db/types.ts +16 -5
- package/src/server/files/db.ts +182 -0
- package/src/server/files/host.ts +496 -0
- package/src/server/files/models.ts +116 -0
- package/src/server/host.ts +17 -4
- package/src/server/inject.ts +1 -0
- package/src/server/runtime.ts +41 -0
- package/src/server/validate.ts +5 -0
- package/src/version.ts +1 -1
package/src/server/runtime.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { AsyncLocalStorage } from "node:async_hooks"
|
|
|
14
14
|
import type { ServerManifest } from "../compile/serverSplit"
|
|
15
15
|
import type { AuthHost } from "./auth/host"
|
|
16
16
|
import type { AuthState } from "./auth/types"
|
|
17
|
+
import type { FilesHost } from "./files/host"
|
|
17
18
|
import { isChannel, isChannelGroup, setChannelPublisher, type ChannelRecord } from "./channel"
|
|
18
19
|
import { setRequestProvider, type RequestContext } from "./context"
|
|
19
20
|
import type { Db } from "./db/types"
|
|
@@ -69,6 +70,9 @@ export type InvokerOptions = {
|
|
|
69
70
|
onError?: (id: string, error: unknown) => void
|
|
70
71
|
/** The auth host (./auth/host.ts): resolves the bearer token into `ctx.auth` before each call. Absent = no sessions. */
|
|
71
72
|
auth?: AuthHost | null
|
|
73
|
+
/** The files host (./files/host.ts): the uploads of a call become `File` arguments, and what the call
|
|
74
|
+
* left unused — or deleted — is cleaned up after it. Absent = a call with files is refused. */
|
|
75
|
+
files?: FilesHost | null
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
// The bundle's ApiError is a different class object than ours (its own SDK copy) — duck-type it.
|
|
@@ -96,6 +100,7 @@ export const createInvoker = (server: LoadedServer, opts: InvokerOptions = {}) =
|
|
|
96
100
|
const onError = opts.onError ?? ((id, e) => console.error(`[server] ${id}:`, e))
|
|
97
101
|
const params = server.manifest.params as Record<string, ParamSchema[]> | undefined
|
|
98
102
|
const auth = opts.auth ?? null
|
|
103
|
+
const files = opts.files ?? null
|
|
99
104
|
|
|
100
105
|
return async (id: string, args: unknown[], ctx: Omit<RequestContext, "id"> = { headers: {} }): Promise<InvokeResult> => {
|
|
101
106
|
const fn = server.endpoints.get(id)
|
|
@@ -103,6 +108,9 @@ export const createInvoker = (server: LoadedServer, opts: InvokerOptions = {}) =
|
|
|
103
108
|
if (!Array.isArray(args)) return { ok: false, status: 400, message: "args must be an array" }
|
|
104
109
|
const rc: RequestContext = { ...ctx, id }
|
|
105
110
|
try {
|
|
111
|
+
// an upload is an argument like any other once it is adopted: validated, then passed
|
|
112
|
+
if (files) args = await files.adopt(args, rc)
|
|
113
|
+
else if (rc.uploads?.length) return { ok: false, status: 400, message: "this server takes no files" }
|
|
106
114
|
if (validate && params?.[id]) validateArgs(params[id], args)
|
|
107
115
|
if (auth) await auth.resolve(rc)
|
|
108
116
|
const result = await als.run(rc, () => fn(...args))
|
|
@@ -113,6 +121,39 @@ export const createInvoker = (server: LoadedServer, opts: InvokerOptions = {}) =
|
|
|
113
121
|
if (e instanceof ValidationError) return { ok: false, status: 400, message: e.message }
|
|
114
122
|
onError(id, e)
|
|
115
123
|
return withSession({ ok: false, status: 500, message: "Internal error" }, rc)
|
|
124
|
+
} finally {
|
|
125
|
+
// in the request's scope: the urls of what the call returned are already made, this is the cleanup
|
|
126
|
+
if (files) await als.run(rc, () => files.finish(rc))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A file field of one row written from OUTSIDE the project's code — a data browser's form. It goes
|
|
133
|
+
* the way an endpoint's write does, through the bundle's own db: `args` are `[model, id, key, value]`,
|
|
134
|
+
* the value what an endpoint would write to the field — an upload of this call (`{ "$file": i }`), a
|
|
135
|
+
* file the row already has (`{ url }`, kept), a list of them, or null. So the field's rule (`t.image()`),
|
|
136
|
+
* the quota and the removal of what is no longer listed are the same.
|
|
137
|
+
*/
|
|
138
|
+
export const createFileWriter = (server: LoadedServer, opts: { files?: FilesHost | null } = {}) => {
|
|
139
|
+
installProvider()
|
|
140
|
+
const files = opts.files ?? null
|
|
141
|
+
return async (args: unknown[], ctx: Omit<RequestContext, "id"> = { headers: {} }): Promise<InvokeResult> => {
|
|
142
|
+
const refuse = (message: string): InvokeResult => ({ ok: false, status: 400, message })
|
|
143
|
+
if (!files?.enabled) return refuse("this project stores no files — a model needs a t.file() field")
|
|
144
|
+
const rc: RequestContext = { ...ctx, id: "$file" }
|
|
145
|
+
try {
|
|
146
|
+
const [model, id, key, value] = await files.adopt(args, rc)
|
|
147
|
+
const db = server.dbs.find(d => d.$files.some(f => f.model === model && f.key === key))
|
|
148
|
+
if (!db) return refuse(`${model}.${key} is not a file field`)
|
|
149
|
+
const name = model as string
|
|
150
|
+
await als.run(rc, () => (db as any)[name.charAt(0).toLowerCase() + name.slice(1)].update(id, { [key as string]: value }))
|
|
151
|
+
return { ok: true, result: null }
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// whoever asks is the project's developer: the reason as it is
|
|
154
|
+
return { ok: false, status: apiStatus(e) ?? 500, message: (e as Error)?.message ?? String(e) }
|
|
155
|
+
} finally {
|
|
156
|
+
await als.run(rc, () => files.finish(rc))
|
|
116
157
|
}
|
|
117
158
|
}
|
|
118
159
|
}
|
package/src/server/validate.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type Schema =
|
|
|
12
12
|
| { t: "tuple", items: Schema[], rest?: Schema }
|
|
13
13
|
| { t: "object", props: Record<string, Schema>, optional?: string[], index?: Schema }
|
|
14
14
|
| { t: "union", of: Schema[] }
|
|
15
|
+
/** A `File` parameter: an upload of this very call (./files/host.ts) — nothing the app can spell in JSON. */
|
|
16
|
+
| { t: "file" }
|
|
15
17
|
|
|
16
18
|
/** One endpoint parameter: its schema and whether it may be omitted (`?`, default value, rest). */
|
|
17
19
|
export type ParamSchema = { name: string, schema: Schema, optional?: boolean, rest?: boolean }
|
|
@@ -61,6 +63,9 @@ export const check = (schema: Schema, value: unknown, path: string): string | nu
|
|
|
61
63
|
}
|
|
62
64
|
return null
|
|
63
65
|
}
|
|
66
|
+
case "file":
|
|
67
|
+
return (globalThis as { __lecodesFiles?: { uploaded(v: unknown): string | undefined } }).__lecodesFiles?.uploaded(value)
|
|
68
|
+
? null : `${path}: expected a file, got ${typeName(value)}`
|
|
64
69
|
case "union": {
|
|
65
70
|
const errors: string[] = []
|
|
66
71
|
for (const s of schema.of) { const e = check(s, value, path); if (!e) return null; errors.push(e) }
|
package/src/version.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
|
|
5
5
|
// and a host's embedded version must agree on the major for the host to run the bundle; minor
|
|
6
6
|
// and patch never gate anything (features are detected, never versioned).
|
|
7
|
-
export const SDK_VERSION = "2.0.
|
|
7
|
+
export const SDK_VERSION = "2.0.8"
|
|
8
8
|
|
|
9
9
|
/** The major of a semver string, or null when it is not one. */
|
|
10
10
|
export const sdkMajor = (version: string | null | undefined): number | null => {
|