@arkiv-network/sdk 0.8.1-dev.1 → 0.9.0-next.2
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 +180 -61
- package/dist/chains/index.d.ts +17 -17
- package/dist/changeOwnership-Cy9gfyjN.d.cts +1160 -0
- package/dist/changeOwnership-Cy9gfyjN.d.cts.map +1 -0
- package/dist/changeOwnership-gXf-lBqI.d.ts +1160 -0
- package/dist/changeOwnership-gXf-lBqI.d.ts.map +1 -0
- package/dist/entity/index.cjs +4 -10
- package/dist/entity/index.d.cts +3 -3
- package/dist/entity/index.d.ts +3 -3
- package/dist/entity/index.js +3 -4
- package/dist/entity-BM1unrQf.cjs +41 -0
- package/dist/{entity-DJ0Gml9c.cjs → entity-BgaFJSaV.cjs} +4 -4
- package/dist/entity-CFAI1KyG.js +36 -0
- package/dist/entity-CFAI1KyG.js.map +1 -0
- package/dist/{entity-CniBgBSd.js → entity-CGqx_AU0.js} +4 -4
- package/dist/entity-CGqx_AU0.js.map +1 -0
- package/dist/{payload-AfUfSctb.d.ts → expirationTime-C7PZK49K.d.ts} +8 -37
- package/dist/expirationTime-C7PZK49K.d.ts.map +1 -0
- package/dist/expirationTime-CkIGNAg8.cjs +140 -0
- package/dist/{payload-D167BlJR.d.cts → expirationTime-DOB8V4B8.d.cts} +8 -37
- package/dist/expirationTime-DOB8V4B8.d.cts.map +1 -0
- package/dist/{expiry-Cgdb_dbq.js → expirationTime-UKPuA-LM.js} +62 -27
- package/dist/expirationTime-UKPuA-LM.js.map +1 -0
- package/dist/expiry-B48I-yTA.d.cts +40 -0
- package/dist/expiry-B48I-yTA.d.cts.map +1 -0
- package/dist/expiry-S2PUL8Hg.d.ts +40 -0
- package/dist/expiry-S2PUL8Hg.d.ts.map +1 -0
- package/dist/index-C-mMF5WZ.d.cts +735 -0
- package/dist/index-C-mMF5WZ.d.cts.map +1 -0
- package/dist/{index-BkqSnGJH.d.ts → index-C4zZqJ9S.d.ts} +5 -27
- package/dist/index-C4zZqJ9S.d.ts.map +1 -0
- package/dist/{index-CLhsFyVH.d.cts → index-CcVWYB6v.d.cts} +5 -27
- package/dist/index-CcVWYB6v.d.cts.map +1 -0
- package/dist/index-DCx2XC6S.d.ts +489 -0
- package/dist/index-DCx2XC6S.d.ts.map +1 -0
- package/dist/index-DLb7BfIw.d.ts +735 -0
- package/dist/index-DLb7BfIw.d.ts.map +1 -0
- package/dist/index-DzriOUm8.d.cts +489 -0
- package/dist/index-DzriOUm8.d.cts.map +1 -0
- package/dist/index.cjs +162 -146
- package/dist/index.d.cts +180 -133
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +180 -133
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +93 -83
- package/dist/index.js.map +1 -1
- package/dist/params-BuIrJA9a.cjs +45 -0
- package/dist/params-DDJOnsNi.js +22 -0
- package/dist/params-DDJOnsNi.js.map +1 -0
- package/dist/query/index.cjs +9 -4
- package/dist/query/index.d.cts +6 -6
- package/dist/query/index.d.ts +6 -6
- package/dist/query/index.js +4 -5
- package/dist/query-D0KtuFh2.cjs +1625 -0
- package/dist/query-DpRDozmj.js +1417 -0
- package/dist/query-DpRDozmj.js.map +1 -0
- package/dist/schema/index.cjs +46 -0
- package/dist/schema/index.d.cts +2 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +3 -0
- package/dist/{attr-BXTkpnoc.cjs → schema-B9AyXcgU.cjs} +513 -246
- package/dist/{attr-tmyREHtw.js → schema-DY4sqehl.js} +427 -244
- package/dist/schema-DY4sqehl.js.map +1 -0
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +5 -5
- package/dist/types/index.d.ts +5 -5
- package/dist/types/index.js +1 -1
- package/dist/utils/index.cjs +3 -5
- package/dist/utils/index.d.cts +3 -3
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/index.js +3 -3
- package/package.json +9 -12
- package/src/actions/public/getEntity.test.ts +69 -0
- package/src/actions/public/getEntity.ts +34 -20
- package/src/actions/public/predictEntityKeys.ts +8 -7
- package/src/actions/public/query.test.ts +58 -0
- package/src/actions/public/query.ts +41 -13
- package/src/actions/wallet/createEntity.ts +34 -63
- package/src/actions/wallet/executeBatch.test.ts +38 -0
- package/src/actions/wallet/executeBatch.ts +26 -9
- package/src/actions/wallet/extendEntity.ts +9 -8
- package/src/actions/wallet/patchEntity.ts +43 -52
- package/src/clients/createPublicClient.test.ts +10 -2
- package/src/clients/createWalletClient.ts +4 -1
- package/src/clients/decorators/arkivPublic.ts +123 -80
- package/src/clients/decorators/arkivWallet.ts +113 -65
- package/src/entity/create.test.ts +25 -21
- package/src/entity/expiry.ts +8 -6
- package/src/entity/flags.ts +1 -1
- package/src/entity/index.ts +2 -4
- package/src/entity/key.ts +3 -3
- package/src/entity/operations.ts +1 -1
- package/src/errors.ts +2 -11
- package/src/index.ts +18 -6
- package/src/query/builderBase.ts +166 -0
- package/src/query/countBuilder.test.ts +91 -0
- package/src/query/countBuilder.ts +67 -0
- package/src/query/engine.ts +9 -3
- package/src/query/errors.ts +24 -14
- package/src/query/expression.test.ts +129 -21
- package/src/query/expression.ts +323 -94
- package/src/query/index.ts +26 -4
- package/src/query/injection.test.ts +5 -11
- package/src/query/queryBuilder.test.ts +467 -58
- package/src/query/queryBuilder.ts +191 -98
- package/src/query/queryResult.ts +35 -9
- package/src/query/selection.test.ts +278 -24
- package/src/query/selection.ts +427 -86
- package/src/schema/attributes.test.ts +266 -0
- package/src/schema/attributes.ts +212 -0
- package/src/{attr → schema}/codec.test.ts +37 -5
- package/src/{attr → schema}/codec.ts +40 -11
- package/src/schema/column.ts +193 -0
- package/src/{attr → schema}/errors.ts +67 -10
- package/src/schema/index.ts +76 -0
- package/src/{attr → schema}/names.ts +4 -2
- package/src/schema/schema.test.ts +171 -0
- package/src/schema/schema.ts +250 -0
- package/src/{attr → schema}/types.ts +36 -30
- package/src/{attr → schema}/values.test.ts +62 -3
- package/src/{attr → schema}/values.ts +246 -65
- package/src/types/entity.test.ts +1 -44
- package/src/types/entity.ts +9 -55
- package/src/types/events.ts +1 -1
- package/src/types/index.ts +11 -4
- package/src/types/rpcSchema.ts +22 -16
- package/src/utils/arkivTransactions.test.ts +356 -116
- package/src/utils/arkivTransactions.ts +30 -38
- package/src/utils/entities.ts +10 -9
- package/src/utils/expirationTime.ts +5 -5
- package/src/utils/index.ts +0 -1
- package/src/utils/logger.test.ts +45 -0
- package/src/utils/logger.ts +5 -39
- package/src/utils/revert.test.ts +25 -0
- package/src/utils/revert.ts +28 -3
- package/dist/attr/index.cjs +0 -34
- package/dist/attr/index.d.cts +0 -2
- package/dist/attr/index.d.ts +0 -2
- package/dist/attr/index.js +0 -3
- package/dist/attr-tmyREHtw.js.map +0 -1
- package/dist/entity-B0t6sN7F.cjs +0 -71
- package/dist/entity-BIp_df9u.js +0 -67
- package/dist/entity-BIp_df9u.js.map +0 -1
- package/dist/entity-CniBgBSd.js.map +0 -1
- package/dist/expiry-BxOlvVWC.d.ts +0 -79
- package/dist/expiry-BxOlvVWC.d.ts.map +0 -1
- package/dist/expiry-CG9GhVXv.cjs +0 -135
- package/dist/expiry-Cgdb_dbq.js.map +0 -1
- package/dist/expiry-DRV29F1r.d.cts +0 -79
- package/dist/expiry-DRV29F1r.d.cts.map +0 -1
- package/dist/flags-DCD26uhe.cjs +0 -43
- package/dist/flags-DUT2gHEU.js +0 -33
- package/dist/flags-DUT2gHEU.js.map +0 -1
- package/dist/index-BkqSnGJH.d.ts.map +0 -1
- package/dist/index-C7fbakoI.d.cts +0 -278
- package/dist/index-C7fbakoI.d.cts.map +0 -1
- package/dist/index-CIcHPx2j.d.cts +0 -424
- package/dist/index-CIcHPx2j.d.cts.map +0 -1
- package/dist/index-CLhsFyVH.d.cts.map +0 -1
- package/dist/index-Cd2aaWme.d.ts +0 -424
- package/dist/index-Cd2aaWme.d.ts.map +0 -1
- package/dist/index-Cq6fa8AI.d.ts +0 -278
- package/dist/index-Cq6fa8AI.d.ts.map +0 -1
- package/dist/index-DaNeJUw8.d.ts +0 -955
- package/dist/index-DaNeJUw8.d.ts.map +0 -1
- package/dist/index-Dv16LO2s.d.cts +0 -955
- package/dist/index-Dv16LO2s.d.cts.map +0 -1
- package/dist/payload-AfUfSctb.d.ts.map +0 -1
- package/dist/payload-D167BlJR.d.cts.map +0 -1
- package/dist/payload-DE7wMztL.js +0 -92
- package/dist/payload-DE7wMztL.js.map +0 -1
- package/dist/payload-DqFweqiO.cjs +0 -108
- package/dist/query-9oojUdM7.cjs +0 -1128
- package/dist/query-Ber-lOJQ.js +0 -968
- package/dist/query-Ber-lOJQ.js.map +0 -1
- package/src/actions/public/getEntityCount.ts +0 -10
- package/src/attr/attributes.test.ts +0 -120
- package/src/attr/attributes.ts +0 -196
- package/src/attr/index.ts +0 -50
- package/src/types/mimeTypes.ts +0 -27
- package/src/utils/compression.test.ts +0 -32
- package/src/utils/compression.ts +0 -102
- package/src/utils/payload.ts +0 -36
- /package/src/{attr → schema}/decimal.ts +0 -0
package/src/query/selection.ts
CHANGED
|
@@ -1,85 +1,246 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type AnyColumn,
|
|
3
|
+
type ArkivValue,
|
|
4
|
+
type AttributeSchema,
|
|
5
|
+
type Attributes,
|
|
6
|
+
type ColumnsOf,
|
|
7
|
+
describeColumns,
|
|
8
|
+
lookupColumn,
|
|
9
|
+
SCHEMA_ATTRIBUTE,
|
|
10
|
+
type Schema,
|
|
11
|
+
SchemaMismatchError,
|
|
12
|
+
type StrValue,
|
|
13
|
+
type TypeTag,
|
|
14
|
+
type ValueOf,
|
|
15
|
+
validateAttributeName,
|
|
16
|
+
} from "../schema"
|
|
17
|
+
import type { Entity, EntityFields } from "../types/entity"
|
|
3
18
|
import type { RpcSelect } from "../types/rpcSchema"
|
|
4
19
|
|
|
5
20
|
/**
|
|
6
|
-
* The entity fields a
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
* The entity fields a row can carry. Each is `$`-prefixed, like the system attributes `where`
|
|
22
|
+
* filters on: an attribute name cannot start with `$`, so no attribute can collide with one.
|
|
23
|
+
*/
|
|
24
|
+
export const SYSTEM_FIELDS = [
|
|
25
|
+
"$key",
|
|
26
|
+
"$owner",
|
|
27
|
+
"$creator",
|
|
28
|
+
"$createdAt",
|
|
29
|
+
"$updatedAt",
|
|
30
|
+
"$expiresAt",
|
|
31
|
+
"$creationFlags",
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
/** The name of an entity field a row carries alongside its attributes. */
|
|
35
|
+
export type SystemField = (typeof SYSTEM_FIELDS)[number]
|
|
36
|
+
|
|
37
|
+
/** The {@link Entity} property a system field reads. */
|
|
38
|
+
type EntityField<F extends SystemField> = F extends `$${infer N extends keyof EntityFields}`
|
|
39
|
+
? N
|
|
40
|
+
: never
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The keys a selection gives a fixed meaning: the entity fields, plus `$attributes` and
|
|
44
|
+
* `$attributeSchema`, which read an entity's attributes as a whole. Any key without a `$` names an
|
|
45
|
+
* attribute.
|
|
46
|
+
*/
|
|
47
|
+
export type SelectionFields = { [K in SystemField]?: boolean } & {
|
|
48
|
+
/**
|
|
49
|
+
* Attribute names, types and indexing, without their values — a cheap way to find out what
|
|
50
|
+
* entities hold before fetching values that may be large. See {@link AttributeSchema}.
|
|
51
|
+
*/
|
|
52
|
+
$attributeSchema?: boolean
|
|
53
|
+
/** Every attribute, as a map keyed by name. */
|
|
54
|
+
$attributes?: boolean
|
|
24
55
|
}
|
|
25
56
|
|
|
26
|
-
/** The selection keys, in the order they are written into the RPC `select
|
|
57
|
+
/** The selection keys with a fixed meaning, in the order they are written into the RPC `select`. */
|
|
27
58
|
const SELECTION_KEYS = [
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"createdAt",
|
|
32
|
-
"updatedAt",
|
|
33
|
-
"expiresAt",
|
|
34
|
-
"creationFlags",
|
|
35
|
-
"contentType",
|
|
36
|
-
"payload",
|
|
37
|
-
"attributeSchema",
|
|
38
|
-
"attributes",
|
|
59
|
+
...SYSTEM_FIELDS,
|
|
60
|
+
"$attributeSchema",
|
|
61
|
+
"$attributes",
|
|
39
62
|
] as const satisfies readonly (keyof SelectionFields)[]
|
|
40
63
|
|
|
64
|
+
/** A selection key with a fixed meaning. */
|
|
65
|
+
type SelectionKey = (typeof SELECTION_KEYS)[number]
|
|
66
|
+
|
|
41
67
|
/**
|
|
42
|
-
*
|
|
43
|
-
* `
|
|
68
|
+
* A selection: entity fields, `$attributes` / `$attributeSchema`, and attributes by name, each set
|
|
69
|
+
* to `true` to ask for it. The same selection means the same thing with or without a schema —
|
|
70
|
+
* naming one with `.from(...)` only types and checks the selected attributes, and the filter. A
|
|
71
|
+
* named schema also restricts the query to entities tagged with its name, and leaves the tag out of
|
|
72
|
+
* every row.
|
|
44
73
|
*/
|
|
45
|
-
type
|
|
46
|
-
|
|
47
|
-
}
|
|
74
|
+
export type Selection = Readonly<Record<string, boolean | undefined>>
|
|
75
|
+
|
|
76
|
+
/** Argument accepted by `select()` — a {@link Selection}, or `"*"` for everything. */
|
|
77
|
+
export type SelectArg = Selection | "*"
|
|
78
|
+
|
|
79
|
+
/** Whether a selection value asks for the field: `true` does, `false` and `undefined` do not. */
|
|
80
|
+
export type Selected<V> = V extends false | undefined ? false : true
|
|
48
81
|
|
|
49
82
|
/**
|
|
50
|
-
*
|
|
51
|
-
* selected.
|
|
83
|
+
* What a column reads back as: a value of its type, or `undefined` too if the column is optional.
|
|
52
84
|
*/
|
|
53
|
-
export type
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
export type ColumnValue<C> = C extends {
|
|
86
|
+
readonly type: infer T extends TypeTag
|
|
87
|
+
readonly isOptional: infer O
|
|
88
|
+
}
|
|
89
|
+
? ValueOf<T> | (true extends O ? undefined : never)
|
|
90
|
+
: never
|
|
57
91
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
92
|
+
/**
|
|
93
|
+
* What a selected key reads back as: an entity field as the entity has it, the attributes as a
|
|
94
|
+
* whole, or one attribute — typed by its column through a schema, and any value or none without.
|
|
95
|
+
*/
|
|
96
|
+
type FieldValue<K, TSchema extends Schema | undefined> = K extends SystemField
|
|
97
|
+
? NonNullable<Entity[EntityField<K>]>
|
|
98
|
+
: K extends "$attributes"
|
|
99
|
+
? Attributes
|
|
100
|
+
: K extends "$attributeSchema"
|
|
101
|
+
? AttributeSchema
|
|
102
|
+
: K extends `$${string}`
|
|
103
|
+
? never
|
|
104
|
+
: TSchema extends Schema
|
|
105
|
+
? K extends keyof ColumnsOf<TSchema>
|
|
106
|
+
? ColumnValue<ColumnsOf<TSchema>[K]>
|
|
107
|
+
: never
|
|
108
|
+
: ArkivValue | undefined
|
|
60
109
|
|
|
61
110
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
111
|
+
* One result of selection `S`, through schema `TSchema` or without one: the selected entity fields
|
|
112
|
+
* and attributes side by side, flat. Through a schema each attribute is typed as its column
|
|
113
|
+
* declares; without one it is whatever value the entity holds, or `undefined`. Accessing an
|
|
114
|
+
* unselected field is a compile error.
|
|
65
115
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
116
|
+
* @example
|
|
117
|
+
* // select({ $key: true, title: true }) → { $key: Hex; title: ArkivValue | undefined }
|
|
118
|
+
* // select({ $key: true, title: true }).from(Posts) → { $key: Hex; title: StrValue }
|
|
68
119
|
*/
|
|
69
|
-
export type
|
|
70
|
-
readonly [K in keyof S as Selected<S[K]> extends true ? K : never]: K
|
|
71
|
-
|
|
72
|
-
: never
|
|
73
|
-
} & (S extends { payload: true } ? Pick<Entity, "toText" | "toJson"> : unknown)
|
|
120
|
+
export type Row<S, TSchema extends Schema | undefined = undefined> = {
|
|
121
|
+
readonly [K in keyof S as Selected<S[K]> extends true ? K : never]: FieldValue<K, TSchema>
|
|
122
|
+
}
|
|
74
123
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
124
|
+
/**
|
|
125
|
+
* What `select()` / `select("*")` selects without a schema: every entity field and every
|
|
126
|
+
* attribute. Not the `$attributeSchema`: its point is learning an entity's shape without fetching
|
|
127
|
+
* its values, so returning it next to every value would only add to the response. It is only
|
|
128
|
+
* returned when asked for by name.
|
|
129
|
+
*/
|
|
130
|
+
type AllSelected = { [K in SystemField | "$attributes"]: true }
|
|
77
131
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The result when everything is selected without a schema — every entity field, and every
|
|
134
|
+
* attribute as the `$attributes` map. Also what `getEntity(key)` returns.
|
|
135
|
+
*/
|
|
136
|
+
export type FullEntity = Row<AllSelected>
|
|
137
|
+
|
|
138
|
+
/** The row type when everything is selected through a schema — every entity field and column. */
|
|
139
|
+
export type FullRow<TSchema extends Schema> = Row<
|
|
140
|
+
{ [K in SystemField | keyof ColumnsOf<TSchema>]: true },
|
|
141
|
+
TSchema
|
|
142
|
+
>
|
|
143
|
+
|
|
144
|
+
declare const dynamic: unique symbol
|
|
80
145
|
|
|
81
146
|
/**
|
|
82
|
-
*
|
|
147
|
+
* The selection type of a builder whose selection is only known at runtime — `select(sel)` with
|
|
148
|
+
* `sel` typed {@link SelectArg} rather than written inline. Nothing about which fields will be
|
|
149
|
+
* present can be known statically, so results are typed with every field optional.
|
|
150
|
+
*/
|
|
151
|
+
export type DynamicSelection = { readonly [dynamic]: true }
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A result whose selection is only known at runtime: any entity field and any attribute may be
|
|
155
|
+
* absent, because nothing says statically which were selected.
|
|
156
|
+
*/
|
|
157
|
+
export type PartialRow<TSchema extends Schema | undefined = undefined> = {
|
|
158
|
+
readonly [K in SystemField]?: NonNullable<Entity[EntityField<K>]>
|
|
159
|
+
} & {
|
|
160
|
+
readonly $attributes?: Attributes
|
|
161
|
+
readonly $attributeSchema?: AttributeSchema
|
|
162
|
+
} & (TSchema extends Schema
|
|
163
|
+
? { readonly [K in keyof ColumnsOf<TSchema>]?: ValueOf<ColumnsOf<TSchema>[K]["type"]> }
|
|
164
|
+
: { readonly [name: string]: ArkivValue | undefined })
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether which fields `S` selects is only known at runtime: a selection typed {@link SelectArg} or
|
|
168
|
+
* {@link Selection}, or one with a field typed `boolean` rather than `true` or `false`.
|
|
169
|
+
*/
|
|
170
|
+
type IsDynamic<S> = string extends keyof Exclude<S, "*">
|
|
171
|
+
? true
|
|
172
|
+
: S extends "*"
|
|
173
|
+
? false
|
|
174
|
+
: true extends {
|
|
175
|
+
[K in keyof S]: [S[K]] extends [boolean] ? (boolean extends S[K] ? true : false) : false
|
|
176
|
+
}[keyof S]
|
|
177
|
+
? true
|
|
178
|
+
: false
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The selection type a query carries for the selection `select()` or `query()` was given: `"*"`
|
|
182
|
+
* for everything, {@link DynamicSelection} when the fields are only known at runtime, and the
|
|
183
|
+
* selection itself otherwise.
|
|
184
|
+
*/
|
|
185
|
+
export type SelectionOf<S> = [S] extends ["*"]
|
|
186
|
+
? "*"
|
|
187
|
+
: IsDynamic<S> extends true
|
|
188
|
+
? DynamicSelection
|
|
189
|
+
: S
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Rejects a selection that asks for nothing at compile time — `{}`, or every field `false`:
|
|
193
|
+
* `unknown` when `S` asks for something, or when that is only known at runtime, and an object no
|
|
194
|
+
* selection satisfies otherwise.
|
|
195
|
+
*/
|
|
196
|
+
export type NonEmptySelection<S> = SelectionOf<S> extends "*" | DynamicSelection
|
|
197
|
+
? unknown
|
|
198
|
+
: [keyof { [K in keyof S as Selected<S[K]> extends true ? K : never]: true }] extends [never]
|
|
199
|
+
? { "select() needs at least one field set to true": true }
|
|
200
|
+
: unknown
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Rejects a `$` key that is not an entity field, `$attributes` or `$attributeSchema` at compile
|
|
204
|
+
* time. An attribute name cannot start with `$`, so such a key can only be a typo: `unknown` when
|
|
205
|
+
* there is none, and an object naming the offenders, which no selection satisfies, otherwise.
|
|
206
|
+
*/
|
|
207
|
+
export type KnownSystemFields<S> = SelectionOf<S> extends "*" | DynamicSelection
|
|
208
|
+
? unknown
|
|
209
|
+
: [Extract<Exclude<keyof S, SelectionKey>, `$${string}`>] extends [never]
|
|
210
|
+
? unknown
|
|
211
|
+
: {
|
|
212
|
+
"selected names that are not entity fields": Extract<
|
|
213
|
+
Exclude<keyof S, SelectionKey>,
|
|
214
|
+
`$${string}`
|
|
215
|
+
>
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** What a query returns per result — a flat row, typed by the schema when there is one. */
|
|
219
|
+
export type ResultOf<
|
|
220
|
+
TSelection,
|
|
221
|
+
TSchema extends Schema | undefined,
|
|
222
|
+
> = TSelection extends DynamicSelection
|
|
223
|
+
? PartialRow<TSchema>
|
|
224
|
+
: TSelection extends "*"
|
|
225
|
+
? TSchema extends Schema
|
|
226
|
+
? FullRow<TSchema>
|
|
227
|
+
: FullEntity
|
|
228
|
+
: Row<TSelection, TSchema>
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The keys of a selection that mean nothing through `TSchema` — neither an entity field,
|
|
232
|
+
* `$attributes` / `$attributeSchema`, nor a column. None for a star or a selection only known at
|
|
233
|
+
* runtime, which `from()` checks when it runs instead.
|
|
234
|
+
*/
|
|
235
|
+
export type UnknownSelection<TSelection, TSchema extends Schema> = TSelection extends
|
|
236
|
+
| "*"
|
|
237
|
+
| DynamicSelection
|
|
238
|
+
? never
|
|
239
|
+
: Exclude<keyof TSelection, SelectionKey | keyof ColumnsOf<TSchema>>
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Converts a selection into the RPC `select` object — the same request {@link projectSelection}
|
|
243
|
+
* makes without a schema.
|
|
83
244
|
*
|
|
84
245
|
* Every field is opt-in — anything not explicitly selected is sent as `false`, including the key.
|
|
85
246
|
* Sending the full object rather than omitting the `false`s keeps what the SDK asked for legible in
|
|
@@ -87,42 +248,222 @@ export type FullEntity = ProjectedEntity<AllSelected>
|
|
|
87
248
|
*
|
|
88
249
|
* `undefined` or `"*"` selects everything.
|
|
89
250
|
*
|
|
90
|
-
* @throws If the selection asks for nothing.
|
|
251
|
+
* @throws If the selection asks for nothing, or names an attribute outside the name grammar.
|
|
91
252
|
*
|
|
92
253
|
* @example
|
|
93
|
-
* toRpcSelect()
|
|
94
|
-
* toRpcSelect({ key: true })
|
|
95
|
-
* toRpcSelect({
|
|
254
|
+
* toRpcSelect() // everything
|
|
255
|
+
* toRpcSelect({ $key: true }) // only the key
|
|
256
|
+
* toRpcSelect({ $key: true, projectId: true }) // the key and one attribute
|
|
96
257
|
*/
|
|
97
258
|
export function toRpcSelect(selection?: SelectArg): RpcSelect {
|
|
259
|
+
return projectSelection(selection).select
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A frozen copy of a selection, so that editing the object passed to `select()` afterwards cannot
|
|
264
|
+
* change what the builder sends.
|
|
265
|
+
*/
|
|
266
|
+
export function snapshotSelection<S extends SelectArg | undefined>(selection: S): S {
|
|
267
|
+
if (typeof selection !== "object" || selection === null) return selection
|
|
268
|
+
return Object.freeze({ ...selection }) as S
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* A selection resolved, through a schema or without one: what to ask the node for, and how to lay
|
|
273
|
+
* the answer out.
|
|
274
|
+
*/
|
|
275
|
+
export type Projection = {
|
|
276
|
+
readonly select: RpcSelect
|
|
277
|
+
/** The entity fields each row carries. */
|
|
278
|
+
readonly fields: readonly SystemField[]
|
|
279
|
+
/**
|
|
280
|
+
* The attributes each row carries as fields of their own, by name — each with its column
|
|
281
|
+
* through a schema, which the value is checked against, and `undefined` without one.
|
|
282
|
+
*/
|
|
283
|
+
readonly columns: readonly (readonly [name: string, column: AnyColumn | undefined])[]
|
|
284
|
+
/** Whether each row carries the `$attributes` map. */
|
|
285
|
+
readonly attributes: boolean
|
|
286
|
+
/** Whether each row carries the `$attributeSchema`. */
|
|
287
|
+
readonly attributeSchema: boolean
|
|
288
|
+
/** The tag each entity must carry, for a named schema. Checked, never part of the row. */
|
|
289
|
+
readonly tag: StrValue | undefined
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Resolves a selection, through a schema or without one. A key that does not start with `$` names
|
|
294
|
+
* an attribute — through a schema, it must be a column.
|
|
295
|
+
*
|
|
296
|
+
* `undefined` or `"*"` selects every entity field and, through a schema, every column; without one,
|
|
297
|
+
* every attribute as the `$attributes` map. The `$attributeSchema` is only selected by name.
|
|
298
|
+
*
|
|
299
|
+
* Attributes are fetched by name where possible, so the ones a row does not carry stay on the node.
|
|
300
|
+
* A named schema's tag is fetched too, so each entity can be checked against it.
|
|
301
|
+
*
|
|
302
|
+
* @throws If the selection asks for nothing, names a `$` key that is not an entity field, names an
|
|
303
|
+
* attribute outside the name grammar, or — through a schema — names an attribute that is not a
|
|
304
|
+
* column.
|
|
305
|
+
*/
|
|
306
|
+
export function projectSelection(selection?: SelectArg, schema?: Schema): Projection {
|
|
307
|
+
const fields: SystemField[] = []
|
|
308
|
+
const columns: [string, AnyColumn | undefined][] = []
|
|
309
|
+
let attributes = false
|
|
310
|
+
let attributeSchema = false
|
|
311
|
+
|
|
98
312
|
if (selection === undefined || selection === "*") {
|
|
99
|
-
|
|
313
|
+
fields.push(...SYSTEM_FIELDS)
|
|
314
|
+
if (schema === undefined) {
|
|
315
|
+
attributes = true
|
|
316
|
+
} else {
|
|
317
|
+
columns.push(...Object.entries(schema.columns))
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
if (typeof selection !== "object" || selection === null || Array.isArray(selection)) {
|
|
321
|
+
throw new TypeError('select() takes "*" or an object of fields set to true')
|
|
322
|
+
}
|
|
323
|
+
for (const [name, wanted] of Object.entries(selection)) {
|
|
324
|
+
if (typeof wanted !== "boolean" && wanted !== undefined) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
name === "$attributes"
|
|
327
|
+
? 'select() takes true or false for "$attributes", which reads every attribute. To ' +
|
|
328
|
+
"read some of them, select each by name: select({ title: true, body: true })."
|
|
329
|
+
: `select() takes true or false for "${name}", got ${typeof wanted}.`,
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
const fixed = (SELECTION_KEYS as readonly string[]).includes(name)
|
|
333
|
+
if (!fixed && name.startsWith("$")) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`select() names "${name}", which is not an entity field. The entity fields are ` +
|
|
336
|
+
`${SYSTEM_FIELDS.join(", ")}, and $attributes and $attributeSchema read the ` +
|
|
337
|
+
"attributes as a whole.",
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
const column = fixed || schema === undefined ? undefined : lookupColumn(schema, name)
|
|
341
|
+
if (!fixed) {
|
|
342
|
+
if (schema !== undefined && column === undefined) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`select() names "${name}", which is not a column of the schema. Its columns are ` +
|
|
345
|
+
`${describeColumns(schema)}.`,
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
validateAttributeName(name)
|
|
349
|
+
}
|
|
350
|
+
if (!wanted) continue
|
|
351
|
+
if (name === "$attributes") attributes = true
|
|
352
|
+
else if (name === "$attributeSchema") attributeSchema = true
|
|
353
|
+
else if (fixed) fields.push(name as SystemField)
|
|
354
|
+
else columns.push([name, column])
|
|
355
|
+
}
|
|
356
|
+
// Rejects both `{}` and a selection whose fields are all explicitly `false` — the type system
|
|
357
|
+
// permits `select({ $key: false })`, and the node would answer it with a page of empty objects.
|
|
358
|
+
if (fields.length === 0 && columns.length === 0 && !attributes && !attributeSchema) {
|
|
359
|
+
throw nothingSelected()
|
|
360
|
+
}
|
|
100
361
|
}
|
|
101
362
|
|
|
102
363
|
const select: RpcSelect = {}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
364
|
+
for (const field of SYSTEM_FIELDS) select[entityField(field)] = fields.includes(field)
|
|
365
|
+
select.attributeSchema = attributeSchema
|
|
366
|
+
if (attributes) {
|
|
367
|
+
select.attributes = true
|
|
368
|
+
} else {
|
|
369
|
+
const names = new Set(columns.map(([name]) => name))
|
|
370
|
+
if (schema?.tag !== undefined) names.add(SCHEMA_ATTRIBUTE)
|
|
371
|
+
select.attributes =
|
|
372
|
+
names.size > 0 ? Object.fromEntries([...names].map((n) => [n, true])) : false
|
|
373
|
+
}
|
|
374
|
+
return { select, fields, columns, attributes, attributeSchema, tag: schema?.tag }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Lays an entity out as a row of `projection` — checking it against the schema, when there is one.
|
|
379
|
+
*
|
|
380
|
+
* @throws {SchemaMismatchError} If a named schema's entity is not tagged with its name, or the
|
|
381
|
+
* entity lacks a selected required column or holds one with a type other than the schema's — it
|
|
382
|
+
* was not written through this schema, or was patched since.
|
|
383
|
+
*/
|
|
384
|
+
export function toRow(entity: Entity, projection: Projection): Record<string, unknown> {
|
|
385
|
+
const subject = entity.key === undefined ? "the entity" : `entity ${entity.key}`
|
|
386
|
+
|
|
387
|
+
const { tag } = projection
|
|
388
|
+
if (tag !== undefined) {
|
|
389
|
+
const found = attributeOf(entity, SCHEMA_ATTRIBUTE)
|
|
390
|
+
if (found?.type !== "str" || found.value !== tag.value) {
|
|
391
|
+
throw new SchemaMismatchError(
|
|
392
|
+
SCHEMA_ATTRIBUTE,
|
|
393
|
+
`${subject} is ${describeTag(found)}, not ${JSON.stringify(tag.value)}, so it was not ` +
|
|
394
|
+
"written through this schema",
|
|
395
|
+
)
|
|
115
396
|
}
|
|
116
|
-
select[field] = value === true
|
|
117
|
-
any ||= value === true
|
|
118
397
|
}
|
|
119
398
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
399
|
+
const row: Record<string, unknown> = {}
|
|
400
|
+
for (const field of projection.fields) row[field] = entity[entityField(field)]
|
|
401
|
+
|
|
402
|
+
for (const [name, column] of projection.columns) {
|
|
403
|
+
const value = attributeOf(entity, name)
|
|
404
|
+
if (column !== undefined) {
|
|
405
|
+
if (value === undefined) {
|
|
406
|
+
if (!column.isOptional) {
|
|
407
|
+
throw new SchemaMismatchError(
|
|
408
|
+
name,
|
|
409
|
+
`${subject} has no value for this required ${column.type} column, so it does not ` +
|
|
410
|
+
"match the schema. Declare the column .optional() if entities may leave it unset",
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
} else if (value.type !== column.type) {
|
|
414
|
+
throw new SchemaMismatchError(
|
|
415
|
+
name,
|
|
416
|
+
`${subject} holds a ${value.type}, not the ${column.type} the schema declares`,
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
row[name] = value
|
|
126
421
|
}
|
|
127
|
-
|
|
422
|
+
|
|
423
|
+
if (projection.attributes) {
|
|
424
|
+
row.$attributes = withoutTag(entity.attributes ?? {}, tag)
|
|
425
|
+
}
|
|
426
|
+
if (projection.attributeSchema) {
|
|
427
|
+
row.$attributeSchema = withoutTag(entity.attributeSchema ?? {}, tag)
|
|
428
|
+
}
|
|
429
|
+
return row
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The {@link Entity} property, and RPC `select` key, a system field reads: its name without `$`. */
|
|
433
|
+
function entityField<F extends SystemField>(field: F): EntityField<F> {
|
|
434
|
+
return field.slice(1) as EntityField<F>
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* A copy of `record` without a named schema's tag: the tag is how a query finds the schema's
|
|
439
|
+
* entities, not one of their attributes, so a row never carries it — and a map read back from a
|
|
440
|
+
* row drops into a write without sending the reserved name.
|
|
441
|
+
*/
|
|
442
|
+
function withoutTag<T>(record: Readonly<Record<string, T>>, tag: StrValue | undefined) {
|
|
443
|
+
if (tag === undefined || !Object.hasOwn(record, SCHEMA_ATTRIBUTE)) return record
|
|
444
|
+
const { [SCHEMA_ATTRIBUTE]: _tag, ...rest } = record
|
|
445
|
+
return rest
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The entity's attribute `name`, or `undefined` if it has none. An own-property read: a column may
|
|
450
|
+
* be named `constructor` or `toString`, and an entity that leaves it unset must not find the
|
|
451
|
+
* inherited one instead.
|
|
452
|
+
*/
|
|
453
|
+
function attributeOf(entity: Entity, name: string): ArkivValue | undefined {
|
|
454
|
+
const { attributes } = entity
|
|
455
|
+
return attributes !== undefined && Object.hasOwn(attributes, name) ? attributes[name] : undefined
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** How an entity's schema tag reads in a mismatch — whatever type another writer gave it. */
|
|
459
|
+
function describeTag(found: ArkivValue | undefined): string {
|
|
460
|
+
if (found === undefined) return "not tagged with a schema"
|
|
461
|
+
if (found.type === "str") return `tagged ${JSON.stringify(found.value)}`
|
|
462
|
+
return `tagged with a value of type ${found.type}`
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function nothingSelected(): Error {
|
|
466
|
+
return new Error(
|
|
467
|
+
'select() requires at least one field to be selected. Pass select() or select("*") to select everything.',
|
|
468
|
+
)
|
|
128
469
|
}
|