lecodes-sdk 2.0.6 → 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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The db layer's half of stored files (./models.ts): what turns a file FIELD — a value in a row — into
|
|
3
|
+
* operations on the platform's `File` model, and back.
|
|
4
|
+
*
|
|
5
|
+
* read the field is selected as the reverse relation it is (the `File` rows that point at the
|
|
6
|
+
* row, in order) and comes back as a `StoredFile`, or a list of them
|
|
7
|
+
* insert the row is inserted without the field; each upload's `File` row is then pointed at it
|
|
8
|
+
* (`ref` to the insert's id) — one transaction
|
|
9
|
+
* update the row's files that are no longer listed are deleted, the uploads are pointed at it —
|
|
10
|
+
* one transaction with the update itself
|
|
11
|
+
*
|
|
12
|
+
* An upload written to a `t.image()` field is first made a picture fit to show (the host's work,
|
|
13
|
+
* ./host.ts): that is why a plan is awaited. What it became — its type, its size — goes to its row
|
|
14
|
+
* in the operation that attaches it.
|
|
15
|
+
*
|
|
16
|
+
* A file is written from the FILE's side (its reference to the owner): that is the side the database
|
|
17
|
+
* cascades over. Only an upload of the current request is ever attached: a `StoredFile` the app hands
|
|
18
|
+
* back means "keep this one", and one of another row is not moved — a client that knows the url of
|
|
19
|
+
* someone else's file cannot take it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { MarciOp } from "../db/marci/query"
|
|
23
|
+
import { FILE_MODEL, fileUrl, filesSeam, storedFileId, type FileField, type StoredFile } from "./models"
|
|
24
|
+
|
|
25
|
+
type Plan = { op: MarciOp, after: MarciOp[] }
|
|
26
|
+
|
|
27
|
+
export type FileLayer = {
|
|
28
|
+
/** The operation as the database runs it, and the ones that follow it in the same transaction.
|
|
29
|
+
* `index` is the operation's place in that transaction (what `ref` counts from). */
|
|
30
|
+
plan(op: MarciOp, index: number): Promise<Plan>
|
|
31
|
+
/** The database's answer to a planned operation, as the project reads it. */
|
|
32
|
+
result(op: MarciOp, value: any): any
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FILE_SELECT = { id: true, name: true, type: true, size: true, width: true, height: true, $order: { position: "asc" } }
|
|
36
|
+
|
|
37
|
+
const stored = (row: any): StoredFile => {
|
|
38
|
+
const out: StoredFile = { url: fileUrl(row.id, row.name), name: row.name, type: row.type, size: row.size }
|
|
39
|
+
if (row.width != null) out.width = row.width
|
|
40
|
+
if (row.height != null) out.height = row.height
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `fields`: the schema's `t.file()` fields. `relations`: model → relation field → its target model.
|
|
46
|
+
* `scalars`: model → what a query without a selection returns (file fields included).
|
|
47
|
+
*/
|
|
48
|
+
export const createFileLayer = (
|
|
49
|
+
fields: FileField[],
|
|
50
|
+
relations: Record<string, Record<string, string>>,
|
|
51
|
+
scalars: Record<string, string[]>,
|
|
52
|
+
): FileLayer => {
|
|
53
|
+
const byModel: Record<string, Record<string, FileField>> = {}
|
|
54
|
+
for (const f of fields) (byModel[f.model] ??= {})[f.key] = f
|
|
55
|
+
const hasFiles = (model: string) => model in byModel
|
|
56
|
+
|
|
57
|
+
// ── read ──
|
|
58
|
+
|
|
59
|
+
/** The query with every selected file field as the relation select it is. */
|
|
60
|
+
const select = (model: string, query: Record<string, any>): Record<string, any> => {
|
|
61
|
+
const out: Record<string, any> = {}
|
|
62
|
+
for (const key in query) {
|
|
63
|
+
const value = query[key]
|
|
64
|
+
if (key.charCodeAt(0) === 36 /* $ */) { out[key] = value; continue }
|
|
65
|
+
if (byModel[model]?.[key]) { if (value) out[key] = FILE_SELECT; continue }
|
|
66
|
+
const target = relations[model]?.[key]
|
|
67
|
+
if (!target || !value) { out[key] = value; continue }
|
|
68
|
+
// `relation: true` is "its scalars" to the engine, which knows no file among them
|
|
69
|
+
if (value === true) out[key] = hasFiles(target) ? select(target, Object.fromEntries(scalars[target].map(k => [k, true]))) : true
|
|
70
|
+
else out[key] = typeof value === "object" ? select(target, value) : value
|
|
71
|
+
}
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rows = (model: string, query: Record<string, any>, value: any): any => {
|
|
76
|
+
if (Array.isArray(value)) { for (const row of value) rows(model, query, row); return value }
|
|
77
|
+
if (value === null || typeof value !== "object") return value
|
|
78
|
+
for (const key in query) {
|
|
79
|
+
if (key.charCodeAt(0) === 36) continue
|
|
80
|
+
const field = byModel[model]?.[key]
|
|
81
|
+
if (field) {
|
|
82
|
+
const list = Array.isArray(value[key]) ? value[key].map(stored) : []
|
|
83
|
+
value[key] = field.array ? list : list[0] ?? null
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
const target = relations[model]?.[key]
|
|
87
|
+
if (target && query[key] && typeof query[key] === "object") rows(target, query[key], value[key])
|
|
88
|
+
}
|
|
89
|
+
return value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── write ──
|
|
93
|
+
|
|
94
|
+
/** The row's data without its file fields, and those fields' values as lists. */
|
|
95
|
+
const split = (model: string, data: any): { data: any, files: [FileField, unknown[]][] } => {
|
|
96
|
+
const own = byModel[model]
|
|
97
|
+
if (!own || data === null || typeof data !== "object") return { data, files: [] }
|
|
98
|
+
const rest: Record<string, unknown> = {}
|
|
99
|
+
const files: [FileField, unknown[]][] = []
|
|
100
|
+
for (const key in data) {
|
|
101
|
+
const field = own[key]
|
|
102
|
+
if (!field) { rest[key] = data[key]; continue }
|
|
103
|
+
const value = data[key]
|
|
104
|
+
if (value === undefined) continue
|
|
105
|
+
if (field.array && !Array.isArray(value)) throw new Error(`${model}.${key}: a list of files is expected`)
|
|
106
|
+
files.push([field, field.array ? value : value === null ? [] : [value]])
|
|
107
|
+
}
|
|
108
|
+
return { data: rest, files }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const attach = async (field: FileField, id: string, owner: unknown, position: number): Promise<MarciOp> => {
|
|
112
|
+
const facts = field.image ? await filesSeam()!.image(id, field.image) : {}
|
|
113
|
+
return { model: FILE_MODEL, action: "update", id: { id }, data: { ...facts, [field.owner]: { $connect: owner }, position } }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const insert = async (op: MarciOp, index: number): Promise<Plan> => {
|
|
117
|
+
const { data, files } = split(op.model, op.data)
|
|
118
|
+
const after: MarciOp[] = []
|
|
119
|
+
for (const [field, values] of files) {
|
|
120
|
+
for (let position = 0; position < values.length; position++) {
|
|
121
|
+
const id = filesSeam()?.uploaded(values[position])
|
|
122
|
+
if (!id) throw new Error(`${op.model}.${field.key}: a new row takes uploaded files only — a file that is already stored belongs to its row`)
|
|
123
|
+
after.push(await attach(field, id, { $ref: String(index) }, position))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { op: { ...op, data }, after }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const update = async (op: MarciOp): Promise<Plan> => {
|
|
130
|
+
const { data, files } = split(op.model, op.data)
|
|
131
|
+
const after: MarciOp[] = []
|
|
132
|
+
for (const [field, values] of files) {
|
|
133
|
+
const keep: string[] = []
|
|
134
|
+
const then: MarciOp[] = []
|
|
135
|
+
for (let position = 0; position < values.length; position++) {
|
|
136
|
+
const value = values[position]
|
|
137
|
+
const upload = filesSeam()?.uploaded(value)
|
|
138
|
+
const id = upload ?? storedFileId(value)
|
|
139
|
+
if (!id) throw new Error(`${op.model}.${field.key}: a file is expected — an upload, or the stored file the field already holds`)
|
|
140
|
+
keep.push(id)
|
|
141
|
+
if (upload) then.push(await attach(field, upload, op.id, position))
|
|
142
|
+
// one that stays: its place in the list — and only where it IS this row's file
|
|
143
|
+
else if (field.array) then.push({ model: FILE_MODEL, action: "updateMany", query: { $where: { id, [field.owner]: op.id } }, data: { position } })
|
|
144
|
+
}
|
|
145
|
+
// what the field held and no longer lists is deleted, with its bytes
|
|
146
|
+
after.push({ model: FILE_MODEL, action: "deleteMany", query: { $where: { [field.owner]: op.id, ...(keep.length ? { id: { $notIn: keep } } : {}) } } }, ...then)
|
|
147
|
+
}
|
|
148
|
+
return { op: { ...op, data }, after }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
async plan(op, index) {
|
|
153
|
+
switch (op.action) {
|
|
154
|
+
case "findMany": case "findFirst":
|
|
155
|
+
return { op: { ...op, query: select(op.model, op.query ?? {}) }, after: [] }
|
|
156
|
+
case "count": case "aggregate":
|
|
157
|
+
return { op, after: [] }
|
|
158
|
+
}
|
|
159
|
+
// rows of File go when a row is deleted (itself, or by a cascade) and when a field is rewritten
|
|
160
|
+
const dirty = () => { const seam = filesSeam(); if (seam) seam.dirty = true }
|
|
161
|
+
switch (op.action) {
|
|
162
|
+
case "insert": return insert(op, index)
|
|
163
|
+
case "update": {
|
|
164
|
+
const plan = await update(op)
|
|
165
|
+
if (plan.after.length) dirty()
|
|
166
|
+
return plan
|
|
167
|
+
}
|
|
168
|
+
case "delete": case "deleteMany":
|
|
169
|
+
dirty()
|
|
170
|
+
break
|
|
171
|
+
case "updateMany":
|
|
172
|
+
for (const key in byModel[op.model] ?? {}) {
|
|
173
|
+
if (op.data && key in op.data) throw new Error(`${op.model}.${key}: a file is written to one row — update(id, …), not updateMany`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { op, after: [] }
|
|
177
|
+
},
|
|
178
|
+
result(op, value) {
|
|
179
|
+
return op.action === "findMany" || op.action === "findFirst" ? rows(op.model, op.query ?? {}, value) : value
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
}
|