@xyo-network/payload-builder 3.5.2 → 3.6.0-rc.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/src/Builder.ts CHANGED
@@ -1,96 +1,92 @@
1
1
  import { assertEx } from '@xylabs/assert'
2
- import type { Hash } from '@xylabs/hex'
3
2
  import type {
4
- AnyObject, JsonArray, JsonObject,
5
- } from '@xylabs/object'
6
- import { isJsonObject, omitBy } from '@xylabs/object'
7
- import { PayloadHasher } from '@xyo-network/hash'
8
- import type {
9
- Payload, PayloadWithMeta, WithMeta,
3
+ Hash,
4
+ Hex,
5
+ } from '@xylabs/hex'
6
+ import {
7
+ isHash, isHex, toHex,
8
+ } from '@xylabs/hex'
9
+ import type { AnyObject } from '@xylabs/object'
10
+ import { ObjectHasher } from '@xyo-network/hash'
11
+ import {
12
+ type Payload, StorageMetaConstants, type WithStorageMeta,
10
13
  } from '@xyo-network/payload-model'
11
14
 
12
- import type { WithoutMeta, WithoutSchema } from './BuilderBase.ts'
13
- import { PayloadBuilderBase, removeMetaAndSchema } from './BuilderBase.ts'
15
+ import { PayloadBuilderBase } from './BuilderBase.ts'
14
16
  import type { PayloadBuilderOptions } from './Options.ts'
15
17
 
16
- export interface BuildOptions {
17
- stamp?: boolean
18
- validate?: boolean
19
- }
20
-
21
- const omitByPredicate = <T extends object>(prefix: string) => (_: T[keyof T], key: keyof T) => {
22
- assertEx(typeof key === 'string', () => `Invalid key type [${String(key)}, ${typeof key}]`)
23
- return (key as string).startsWith(prefix)
24
- }
25
-
26
18
  export class PayloadBuilder<
27
19
  T extends Payload = Payload<AnyObject>,
28
20
  O extends PayloadBuilderOptions<T> = PayloadBuilderOptions<T>,
29
21
  > extends PayloadBuilderBase<T, O> {
30
- static async build<T extends Payload = Payload<AnyObject>>(payload: T, options?: BuildOptions): Promise<WithMeta<T>>
31
- static async build<T extends Payload = Payload<AnyObject>>(payload: T[], options?: BuildOptions): Promise<WithMeta<T>[]>
32
- static async build<T extends Payload = Payload<AnyObject>>(payload: T | T[], options: BuildOptions = {}) {
33
- if (Array.isArray(payload)) {
34
- return await Promise.all(payload.map(payload => this.build(payload, options)))
35
- } else {
36
- const { stamp = false, validate = true } = options
37
- const {
38
- schema, $hash: incomingDataHash, $meta: incomingMeta = {},
39
- } = payload as WithMeta<T>
40
-
41
- // check for legacy signatures
42
- const { _signatures } = payload as { _signatures?: JsonArray }
43
- if (_signatures && !incomingMeta.signatures) {
44
- incomingMeta.signatures = _signatures
45
- }
46
-
47
- const fields = removeMetaAndSchema(payload)
48
- const dataHashableFields = await PayloadBuilder.dataHashableFields(schema, fields)
49
- const $hash = validate || incomingDataHash === undefined ? await PayloadHasher.hash(dataHashableFields) : incomingDataHash
50
- const $meta: JsonObject = { ...incomingMeta }
51
- if ($meta.timestamp === undefined && stamp) {
52
- $meta.timestamp = Date.now()
53
- }
54
- const hashableFields: WithMeta<Payload> = {
55
- ...dataHashableFields, $hash, schema,
56
- }
57
-
58
- if (Object.keys($meta).length > 0) {
59
- hashableFields.$meta = $meta
60
- }
61
-
62
- return hashableFields as WithMeta<T>
22
+ static async addSequencedStorageMeta<T extends Payload = Payload>(payload: T, hash?: Hash, dataHash?: Hash): Promise<WithStorageMeta<T>> {
23
+ assertEx(hash === undefined || isHash(hash), () => 'Invalid hash')
24
+ assertEx(dataHash === undefined || isHash(dataHash), () => 'Invalid dataHash')
25
+ const _hash = hash ?? await PayloadBuilder.hash(payload)
26
+ return {
27
+ ...payload,
28
+ _sequence: this.buildSequence(Date.now(), _hash.slice(-(StorageMetaConstants.nonceBytes * 2)) as Hex),
29
+ _dataHash: dataHash ?? await PayloadBuilder.dataHash(payload),
30
+ _hash,
63
31
  }
64
32
  }
65
33
 
66
- static async dataHash<T extends Payload>(payload: T, options?: BuildOptions): Promise<Hash> {
67
- return (await this.build(payload, options)).$hash
34
+ static async addStorageMeta<T extends Payload>(payload: T): Promise<WithStorageMeta<T>>
35
+ static async addStorageMeta<T extends Payload>(payloads: T[]): Promise<WithStorageMeta<T>[]>
36
+ static async addStorageMeta<T extends Payload>(payloads: T | T[]): Promise<WithStorageMeta<T>[] | WithStorageMeta<T>> {
37
+ return Array.isArray(payloads)
38
+ ? await (async () => {
39
+ const pairs = await PayloadBuilder.hashPairs(payloads)
40
+ return await Promise.all(pairs.map(async ([payload, hash]) => await this.addSequencedStorageMeta(
41
+ payload,
42
+ hash,
43
+ )))
44
+ })()
45
+ : this.addSequencedStorageMeta(
46
+ payloads,
47
+ )
48
+ }
49
+
50
+ static buildSequence(epoch: number, nonce: Hex): Hex {
51
+ assertEx(
52
+ epoch <= StorageMetaConstants.maxEpoch,
53
+ () => `epoch must be less than or equal to ${StorageMetaConstants.maxEpoch} [${epoch}]`,
54
+ )
55
+ assertEx(isHex(nonce), () => 'nonce must be a Hex type')
56
+ assertEx(
57
+ nonce.length === StorageMetaConstants.nonceBytes * 2,
58
+ () => `nonce must be ${StorageMetaConstants.nonceBytes} bytes [${nonce.length}] <- Hex String Length`,
59
+ )
60
+ return `${toHex(epoch, { byteSize: 4 })}${nonce}` as Hex
68
61
  }
69
62
 
70
- static async dataHashPairs<T extends Payload>(payloads: T[], options?: BuildOptions): Promise<[WithMeta<T>, Hash][]> {
63
+ static async dataHash<T extends Payload>(payload: T): Promise<Hash> {
64
+ return await ObjectHasher.hash(this.omitMeta(payload))
65
+ }
66
+
67
+ static async dataHashPairs<T extends Payload>(payloads: T[]): Promise<[T, Hash][]> {
71
68
  return await Promise.all(
72
69
  payloads.map(async (payload) => {
73
- const built = await PayloadBuilder.build(payload, options)
74
- return [built, built.$hash]
70
+ const dataHash = await this.dataHash(payload)
71
+ return [payload, dataHash]
75
72
  }),
76
73
  )
77
74
  }
78
75
 
79
- static async dataHashes(payloads: undefined, options?: BuildOptions): Promise<undefined>
80
- static async dataHashes<T extends Payload>(payloads: T[], options?: BuildOptions): Promise<Hash[]>
81
- static async dataHashes<T extends Payload>(payloads?: T[], options?: BuildOptions): Promise<Hash[] | undefined> {
76
+ static async dataHashes(payloads: undefined): Promise<undefined>
77
+ static async dataHashes<T extends Payload>(payloads: T[]): Promise<Hash[]>
78
+ static async dataHashes<T extends Payload>(payloads?: T[]): Promise<Hash[] | undefined> {
82
79
  return payloads
83
80
  ? await Promise.all(
84
81
  payloads.map(async (payload) => {
85
- const built = await PayloadBuilder.build(payload, options)
86
- return built.$hash
82
+ return await PayloadBuilder.dataHash(payload)
87
83
  }),
88
84
  )
89
85
  : undefined
90
86
  }
91
87
 
92
88
  static async filterExclude<T extends Payload>(payloads: T[] = [], hash: Hash[] | Hash): Promise<T[]> {
93
- return await PayloadHasher.filterExcludeByHash(await this.filterExcludeByDataHash(payloads, hash), hash)
89
+ return await ObjectHasher.filterExcludeByHash(await this.filterExcludeByDataHash(payloads, hash), hash)
94
90
  }
95
91
 
96
92
  static async filterExcludeByDataHash<T extends Payload>(payloads: T[] = [], hash: Hash[] | Hash): Promise<T[]> {
@@ -107,8 +103,8 @@ export class PayloadBuilder<
107
103
  return (await this.dataHashPairs(payloads)).find(([_, objHash]) => objHash === hash)?.[0]
108
104
  }
109
105
 
110
- static async hash<T extends Payload>(payload: T, options?: BuildOptions): Promise<Hash> {
111
- return await PayloadHasher.hash(await PayloadBuilder.build(payload, options))
106
+ static async hash<T extends Payload>(payload: T): Promise<Hash> {
107
+ return await ObjectHasher.hash(this.omitStorageMeta(payload))
112
108
  }
113
109
 
114
110
  /**
@@ -116,68 +112,47 @@ export class PayloadBuilder<
116
112
  * @param objs Any array of payloads
117
113
  * @returns An array of payload/hash tuples
118
114
  */
119
- static async hashPairs<T extends Payload>(payloads: T[], options?: BuildOptions): Promise<[WithMeta<T>, Hash][]> {
115
+ static async hashPairs<T extends Payload>(payloads: T[]): Promise<[T, Hash][]> {
120
116
  return await Promise.all(
121
- payloads.map<Promise<[WithMeta<T>, Hash]>>(async (payload) => {
122
- const built = await PayloadBuilder.build(payload, options)
123
- return [built, await PayloadBuilder.hash(built)]
117
+ payloads.map<Promise<[T, Hash]>>(async (payload) => {
118
+ return [payload, await PayloadBuilder.hash(payload)]
124
119
  }),
125
120
  )
126
121
  }
127
122
 
128
- static async hashableFields<T extends Payload = Payload<AnyObject>>(
129
- schema: string,
130
- fields?: WithoutSchema<WithoutMeta<T>>,
131
- $meta?: JsonObject,
132
- $hash?: Hash,
133
- timestamp?: number,
134
- stamp = false,
135
- ): Promise<WithMeta<T>> {
136
- const dataFields = await this.dataHashableFields<T>(schema, fields)
137
- assertEx($meta === undefined || isJsonObject($meta), () => '$meta must be JsonObject')
138
- const result: WithMeta<T> = omitBy(
139
- {
140
- ...dataFields,
141
- $hash: $hash ?? (await PayloadBuilder.dataHash(dataFields)),
142
- schema,
143
- } as WithMeta<T>,
144
- omitByPredicate('_'),
145
- ) as WithMeta<T>
146
-
147
- const clonedMeta = { ...$meta }
148
-
149
- if (timestamp) {
150
- clonedMeta.timestamp = timestamp
151
- }
152
-
153
- if (clonedMeta.timestamp === undefined && stamp) {
154
- clonedMeta.timestamp = Date.now()
155
- }
156
-
157
- if (Object.keys(clonedMeta).length > 0) {
158
- result.$meta = clonedMeta
159
- }
160
-
161
- return result
123
+ static hashableFields<T extends Payload>(
124
+ payload: T,
125
+ ): T {
126
+ return this.omitStorageMeta(payload)
162
127
  }
163
128
 
164
129
  static async hashes(payloads: undefined): Promise<undefined>
165
130
  static async hashes<T extends Payload>(payloads: T[]): Promise<Hash[]>
166
131
  static async hashes<T extends Payload>(payloads?: T[]): Promise<Hash[] | undefined> {
167
- return await PayloadHasher.hashes(payloads)
132
+ return await ObjectHasher.hashes(payloads)
168
133
  }
169
134
 
170
- static async toAllHashMap<T extends Payload>(objs: T[]): Promise<Record<Hash, WithMeta<T>>> {
171
- const result: Record<Hash, WithMeta<T>> = {}
172
- for (const pair of await this.hashPairs(objs)) {
135
+ static sortByStorageMeta<T extends Payload>(payloads: WithStorageMeta<T>[], direction: -1 | 1 = 1) {
136
+ return payloads.sort((a, b) =>
137
+ a._sequence < b._sequence
138
+ ? -direction
139
+ : a._sequence > b._sequence
140
+ ? direction
141
+ : 0)
142
+ }
143
+
144
+ static async toAllHashMap<T extends Payload>(payloads: T[]): Promise<Record<Hash, T>> {
145
+ const result: Record<Hash, T> = {}
146
+ for (const pair of await this.hashPairs(payloads)) {
147
+ const dataHash = await this.dataHash(pair[0])
173
148
  result[pair[1]] = pair[0]
174
- result[pair[0].$hash] = pair[0]
149
+ result[dataHash] = pair[0]
175
150
  }
176
151
  return result
177
152
  }
178
153
 
179
- static async toDataHashMap<T extends Payload>(objs: T[]): Promise<Record<Hash, WithMeta<T>>> {
180
- const result: Record<Hash, WithMeta<T>> = {}
154
+ static async toDataHashMap<T extends Payload>(objs: T[]): Promise<Record<Hash, T>> {
155
+ const result: Record<Hash, T> = {}
181
156
  for (const pair of await this.dataHashPairs(objs)) {
182
157
  result[pair[1]] = pair[0]
183
158
  }
@@ -189,41 +164,18 @@ export class PayloadBuilder<
189
164
  * @param objs Any array of payloads
190
165
  * @returns A map of hashes to payloads
191
166
  */
192
- static async toHashMap<T extends Payload>(objs: T[]): Promise<Record<Hash, WithMeta<T>>> {
193
- const result: Record<Hash, WithMeta<T>> = {}
167
+ static async toHashMap<T extends Payload>(objs: T[]): Promise<Record<Hash, T>> {
168
+ const result: Record<Hash, T> = {}
194
169
  for (const pair of await this.hashPairs(objs)) {
195
170
  result[pair[1]] = pair[0]
196
171
  }
197
172
  return result
198
173
  }
199
174
 
200
- static withoutMeta(payload: undefined): undefined
201
- static withoutMeta<T extends PayloadWithMeta>(payload: T): Omit<T, '$meta'>
202
- static withoutMeta<T extends PayloadWithMeta>(payloads: T[]): Omit<T, '$meta'>[]
203
- static withoutMeta<T extends PayloadWithMeta>(payloads: T | T[]) {
204
- if (Array.isArray(payloads)) {
205
- return payloads.map(payload => this.withoutMeta(payload))
206
- } else {
207
- if (payloads) {
208
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
209
- const { $meta, ...result } = payloads
210
- return result as Omit<T, '$meta'>
211
- }
212
- }
213
- }
214
-
215
- async build(options?: BuildOptions): Promise<WithMeta<T>> {
216
- const dataHashableFields = await this.dataHashableFields()
217
- return await PayloadBuilder.build<T>({
218
- ...dataHashableFields, $meta: this._$meta, schema: this._schema,
219
- } as Payload as T, options)
220
- }
221
-
222
- async hashableFields() {
223
- return await PayloadBuilder.hashableFields(
224
- assertEx(this._schema, () => 'Payload: Missing Schema'),
225
- this._fields,
226
- this._$meta,
227
- )
175
+ build(): T {
176
+ return {
177
+ schema: this._schema,
178
+ ...this._fields,
179
+ } as T
228
180
  }
229
181
  }
@@ -1,98 +1,93 @@
1
1
  import { assertEx } from '@xylabs/assert'
2
- import type { Hash } from '@xylabs/hex'
3
- import type { AnyObject, JsonObject } from '@xylabs/object'
2
+ import type { AnyObject } from '@xylabs/object'
4
3
  import {
5
4
  isJsonObject, omitBy, toJson,
6
5
  } from '@xylabs/object'
7
6
  import type { Promisable } from '@xylabs/promise'
8
7
  import { removeEmptyFields } from '@xyo-network/hash'
9
- import type {
10
- Payload, Schema, WithMeta, WithOptionalMeta,
11
- } from '@xyo-network/payload-model'
8
+ import type { Payload, Schema } from '@xyo-network/payload-model'
12
9
 
10
+ import { PayloadBuilder } from './Builder.ts'
13
11
  import type { PayloadBuilderOptions } from './Options.ts'
14
12
 
15
13
  export type WithOptionalSchema<T extends Payload> = Omit<T, 'schema'> & Partial<T>
16
14
 
17
15
  export type WithoutSchema<T extends WithOptionalSchema<Payload>> = Omit<T, 'schema'>
18
16
 
19
- export type WithoutMeta<T extends WithOptionalMeta<Payload>> = Omit<T, '$hash' | '$meta'>
20
-
21
- export const removeMetaAndSchema = <T extends Payload>(payload: WithOptionalSchema<WithOptionalMeta<T>>): WithoutSchema<WithoutMeta<T>> => {
22
- const { ...result } = payload
23
- delete result.$hash
24
- delete result.$meta
17
+ export const removeMetaAndSchema = <T extends Payload>(payload: Partial<WithOptionalSchema<T>>): WithoutSchema<T> => {
18
+ const { ...result } = PayloadBuilder.omitMeta(payload as T) as WithOptionalSchema<T>
25
19
  delete result.schema
26
20
  return result as Omit<T, 'schema'>
27
21
  }
28
22
 
29
- const omitByPredicate = (prefix: string) => (_: unknown, key: string) => {
23
+ const omitByPrefixPredicate = (prefix: string) => (_: unknown, key: string) => {
30
24
  assertEx(typeof key === 'string', () => `Invalid key type [${key}, ${typeof key}]`)
31
25
  return key.startsWith(prefix)
32
26
  }
33
27
 
34
28
  export class PayloadBuilderBase<T extends Payload = Payload<AnyObject>, O extends PayloadBuilderOptions<T> = PayloadBuilderOptions<T>> {
35
- protected _$meta?: JsonObject
36
- protected _fields?: WithoutSchema<WithoutMeta<T>>
29
+ protected _fields?: Partial<WithoutSchema<T>>
37
30
  protected _schema: Schema
38
31
 
39
32
  constructor(readonly options: O) {
40
- const {
41
- schema, fields, meta,
42
- } = options
33
+ const { schema, fields } = options
43
34
  this._schema = schema
44
- this._fields = removeEmptyFields(fields ?? {}) as WithoutSchema<WithoutMeta<T>>
45
- this._$meta = meta
35
+ this._fields = removeMetaAndSchema(removeEmptyFields(structuredClone(fields ?? {})))
46
36
  }
47
37
 
48
- static dataHashableFields<T extends Payload = Payload<AnyObject>>(
49
- schema: string,
50
- fields?: WithoutSchema<WithoutMeta<T>>,
51
- ): Promisable<Omit<T, '$hash' | '$meta'>> {
52
- const cleanFields = fields ? removeEmptyFields(fields) : undefined
38
+ static dataHashableFields<T extends Payload>(
39
+ schema: Schema,
40
+ payload: WithoutSchema<T>,
41
+
42
+ ): Promisable<Payload> {
43
+ const cleanFields = removeEmptyFields({ ...payload, schema })
53
44
  assertEx(
54
45
  cleanFields === undefined || isJsonObject(cleanFields),
55
46
  () => `Fields must be JsonObject: ${JSON.stringify(toJson(cleanFields), null, 2)}`,
56
47
  )
57
- return omitBy(omitBy({ schema, ...cleanFields }, omitByPredicate('$')), omitByPredicate('_')) as unknown as T
48
+ return this.omitMeta(cleanFields) as T
58
49
  }
59
50
 
60
- protected static metaFields(dataHash: Hash, otherMeta?: JsonObject, stamp = true): Promisable<JsonObject> {
61
- const meta: JsonObject = { ...otherMeta }
62
-
63
- if (!meta.timestamp && stamp) {
64
- meta.timestamp = meta.timestamp ?? Date.now()
65
- }
51
+ static omitClientMeta<T extends Payload>(payload: T, maxDepth?: number): T
52
+ static omitClientMeta<T extends Payload>(payloads: T[], maxDepth?: number): T[]
53
+ static omitClientMeta<T extends Payload>(payloads: T | T[], maxDepth = 100): T | T[] {
54
+ return Array.isArray(payloads)
55
+ ? payloads.map(payload => this.omitClientMeta(payload, maxDepth)) as T[]
56
+ : omitBy(payloads, omitByPrefixPredicate('$'), maxDepth) as T
57
+ }
66
58
 
67
- return meta
59
+ static omitMeta<T extends Payload>(payload: T, maxDepth?: number): T
60
+ static omitMeta<T extends Payload>(payloads: T[], maxDepth?: number): T[]
61
+ static omitMeta<T extends Payload>(payloads: T | T[], maxDepth = 100): T | T[] {
62
+ return Array.isArray(payloads)
63
+ ? this.omitStorageMeta(this.omitClientMeta(payloads, maxDepth), maxDepth)
64
+ : this.omitStorageMeta(this.omitClientMeta(payloads, maxDepth), maxDepth)
68
65
  }
69
66
 
70
- $meta(meta?: JsonObject) {
71
- this._$meta = meta ?? (this._fields as WithMeta<T>).$meta
72
- return this
67
+ static omitStorageMeta<T extends Payload>(payload: T, maxDepth?: number): T
68
+ static omitStorageMeta<T extends Payload>(payloads: T[], maxDepth?: number): T[]
69
+ static omitStorageMeta<T extends Payload>(payloads: T | T[], maxDepth = 100): T | T[] {
70
+ return Array.isArray(payloads)
71
+ ? payloads.map(payload => this.omitStorageMeta(payload, maxDepth)) as T[]
72
+ : omitBy(payloads, omitByPrefixPredicate('_'), maxDepth) as T
73
73
  }
74
74
 
75
75
  async dataHashableFields() {
76
76
  return await PayloadBuilderBase.dataHashableFields(
77
77
  assertEx(this._schema, () => 'Payload: Missing Schema'),
78
- this._fields,
78
+ // TDOD: Add verification that required fields are present
79
+ this._fields as T,
79
80
  )
80
81
  }
81
82
 
82
- // we do not require sending in $hash since it will be generated anyway
83
- fields(fields: WithOptionalSchema<WithOptionalMeta<T>>) {
83
+ fields(fields: WithOptionalSchema<T>) {
84
84
  if (fields) {
85
- const {
86
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
87
- $meta, $hash, schema, ...fieldsOnly
88
- } = fields
89
- if ($meta) {
90
- this.$meta($meta)
91
- }
85
+ const fieldsClone = structuredClone(fields)
86
+ const { schema } = fieldsClone
92
87
  if (schema) {
93
88
  this.schema(schema)
94
89
  }
95
- this._fields = removeMetaAndSchema<T>(fields)
90
+ this._fields = removeEmptyFields(removeMetaAndSchema<T>(fieldsClone))
96
91
  }
97
92
  return this
98
93
  }
@@ -100,8 +95,4 @@ export class PayloadBuilderBase<T extends Payload = Payload<AnyObject>, O extend
100
95
  schema(value: Schema) {
101
96
  this._schema = value
102
97
  }
103
-
104
- protected async metaFields(dataHash: Hash, stamp = true): Promise<JsonObject> {
105
- return await PayloadBuilderBase.metaFields(dataHash, this._$meta, stamp)
106
- }
107
98
  }
package/src/Options.ts CHANGED
@@ -3,7 +3,7 @@ import type { JsonObject } from '@xylabs/object'
3
3
  import type { Schema } from '@xyo-network/payload-model'
4
4
 
5
5
  export interface PayloadBuilderOptions<T> {
6
- readonly fields?: Omit<T, 'schema' | '$hash' | '$meta'>
6
+ readonly fields?: Partial<T>
7
7
  readonly logger?: Logger
8
8
  readonly meta?: JsonObject
9
9
  readonly schema: Schema