@forgehive/task 0.2.6 → 0.3.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/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -40
- package/dist/index.js.map +1 -1
- package/dist/test/safe-replay.test.js +1 -2
- package/dist/test/safe-replay.test.js.map +1 -1
- package/dist/test/validation.test.js +22 -14
- package/dist/test/validation.test.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +85 -46
- package/src/test/safe-replay.test.ts +3 -4
- package/src/test/validation.test.ts +22 -14
- package/dist/test/integration-enhanced-records.test.d.ts +0 -2
- package/dist/test/integration-enhanced-records.test.d.ts.map +0 -1
- package/dist/test/integration-enhanced-records.test.js +0 -467
- package/dist/test/integration-enhanced-records.test.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -156,9 +156,16 @@ export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B ex
|
|
|
156
156
|
// Define a type for the accumulated boundary data
|
|
157
157
|
type BoundaryData = Array<{input: unknown[], output?: unknown}>
|
|
158
158
|
|
|
159
|
-
// Helper type to infer schema type
|
|
159
|
+
// Helper type to infer schema type.
|
|
160
|
+
// An empty schema infers to `{ [k: string]: never }` under zod 4; that falls back
|
|
161
|
+
// to a loose record so tasks can still receive pass-through arguments that aren't
|
|
162
|
+
// declared in the schema.
|
|
160
163
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
|
-
export type InferSchemaType<S> = S extends Schema<any>
|
|
164
|
+
export type InferSchemaType<S> = S extends Schema<any>
|
|
165
|
+
? InferSchema<S> extends Record<string, never>
|
|
166
|
+
? Record<string, unknown>
|
|
167
|
+
: InferSchema<S>
|
|
168
|
+
: Record<string, unknown>;
|
|
162
169
|
|
|
163
170
|
// Type for execution record boundaries that are automatically injected
|
|
164
171
|
// When adding new execution boundaries, add their types here
|
|
@@ -343,28 +350,17 @@ export const Task = class Task<
|
|
|
343
350
|
return result.success ?? false
|
|
344
351
|
}
|
|
345
352
|
|
|
346
|
-
// Helper method to check if schema is empty
|
|
353
|
+
// Helper method to check if schema is empty.
|
|
354
|
+
// Uses the schema's JSON Schema description (the public contract) rather than
|
|
355
|
+
// reaching into zod internals, so it stays correct across zod versions.
|
|
347
356
|
public _isEmptySchema(): boolean {
|
|
348
357
|
if (!this._schema) {
|
|
349
358
|
return false
|
|
350
359
|
}
|
|
351
360
|
|
|
352
|
-
|
|
353
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
354
|
-
const zodSchema = (this._schema as any).schema
|
|
355
|
-
if (!zodSchema || !zodSchema._def) {
|
|
356
|
-
return false
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const shapeFn = zodSchema._def.shape
|
|
360
|
-
if (typeof shapeFn !== 'function') {
|
|
361
|
-
return false
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const shape = shapeFn()
|
|
365
|
-
const isEmpty = Object.keys(shape).length === 0
|
|
361
|
+
const properties = this._schema.describe().properties
|
|
366
362
|
|
|
367
|
-
return
|
|
363
|
+
return !properties || Object.keys(properties).length === 0
|
|
368
364
|
}
|
|
369
365
|
|
|
370
366
|
// Posible improvement to handle multiple listeners, but so far its not needed
|
|
@@ -580,7 +576,7 @@ export const Task = class Task<
|
|
|
580
576
|
if (this._schema) {
|
|
581
577
|
const validation = this._schema.safeParse(normalizedInput)
|
|
582
578
|
if (!validation.success) {
|
|
583
|
-
const errorDetails = validation.error?.
|
|
579
|
+
const errorDetails = validation.error?.issues.map(err =>
|
|
584
580
|
`${err.path.join('.')}: ${err.message}`
|
|
585
581
|
).join(', ')
|
|
586
582
|
|
|
@@ -742,7 +738,7 @@ export const Task = class Task<
|
|
|
742
738
|
if (this._schema) {
|
|
743
739
|
const validation = this._schema.safeParse(argv)
|
|
744
740
|
if (!validation.success) {
|
|
745
|
-
const errorDetails = validation.error?.
|
|
741
|
+
const errorDetails = validation.error?.issues.map(err =>
|
|
746
742
|
`${err.path.join('.')}: ${err.message}`
|
|
747
743
|
).join(', ')
|
|
748
744
|
|
|
@@ -834,7 +830,7 @@ export const Task = class Task<
|
|
|
834
830
|
if (this._schema) {
|
|
835
831
|
const validation = this._schema.safeParse(eventArgs)
|
|
836
832
|
if (!validation.success) {
|
|
837
|
-
const errorDetails = validation.error?.
|
|
833
|
+
const errorDetails = validation.error?.issues.map(err =>
|
|
838
834
|
`${err.path.join('.')}: ${err.message}`
|
|
839
835
|
).join(', ')
|
|
840
836
|
|
|
@@ -846,7 +842,7 @@ export const Task = class Task<
|
|
|
846
842
|
statusCode: 422,
|
|
847
843
|
body: JSON.stringify({
|
|
848
844
|
error: errorMessage,
|
|
849
|
-
details: validation.error?.
|
|
845
|
+
details: validation.error?.issues
|
|
850
846
|
})
|
|
851
847
|
}
|
|
852
848
|
}
|
|
@@ -896,19 +892,58 @@ export const Task = class Task<
|
|
|
896
892
|
const apiKey = process.env.HIVE_API_KEY
|
|
897
893
|
const apiSecret = process.env.HIVE_API_SECRET
|
|
898
894
|
const host = process.env.HIVE_HOST
|
|
899
|
-
const projectName = process.env.HIVE_PROJECT_NAME
|
|
900
|
-
|
|
901
895
|
|
|
902
896
|
// If any required env vars are missing, do nothing
|
|
903
|
-
if (!apiKey || !apiSecret || !host
|
|
897
|
+
if (!apiKey || !apiSecret || !host) {
|
|
898
|
+
// eslint-disable-next-line no-console
|
|
899
|
+
console.log('Missing required env vars for sending log to Hive:', { apiKey: !!apiKey, apiSecret: !!apiSecret, host })
|
|
900
|
+
return
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Load forge.json for UUID-based logging
|
|
904
|
+
let projectUuid: string | null = null
|
|
905
|
+
let taskUuid: string | null = null
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
909
|
+
const fs = require('fs')
|
|
910
|
+
const forgeJsonContent = fs.readFileSync('./forge.json', 'utf8')
|
|
911
|
+
const forgeConfig = JSON.parse(forgeJsonContent)
|
|
912
|
+
|
|
913
|
+
projectUuid = forgeConfig.project?.uuid
|
|
914
|
+
|
|
915
|
+
// Find task UUID by matching task name
|
|
916
|
+
const taskName = this._name || process.env.HIVE_TASK_NAME || 'unnamed-task'
|
|
917
|
+
if (forgeConfig.tasks && forgeConfig.tasks[taskName]) {
|
|
918
|
+
taskUuid = forgeConfig.tasks[taskName].uuid
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// eslint-disable-next-line no-console
|
|
922
|
+
console.log('Loaded forge.json for logging:', { projectUuid: !!projectUuid, taskUuid: !!taskUuid, taskName })
|
|
923
|
+
} catch (error) {
|
|
924
|
+
// eslint-disable-next-line no-console
|
|
925
|
+
console.log('Could not load forge.json, skipping log send:', error instanceof Error ? error.message : 'Unknown error')
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Require both UUIDs for logging
|
|
930
|
+
if (!projectUuid || !taskUuid) {
|
|
904
931
|
// eslint-disable-next-line no-console
|
|
905
|
-
console.log('Missing
|
|
932
|
+
console.log('Missing project or task UUID, skipping log send')
|
|
906
933
|
return
|
|
907
934
|
}
|
|
908
935
|
|
|
909
|
-
|
|
910
|
-
|
|
936
|
+
await this._sendToHiveWithUuid(log, projectUuid, taskUuid, apiKey, apiSecret, host)
|
|
937
|
+
}
|
|
911
938
|
|
|
939
|
+
async _sendToHiveWithUuid(
|
|
940
|
+
log: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>,
|
|
941
|
+
projectUuid: string,
|
|
942
|
+
taskUuid: string,
|
|
943
|
+
apiKey: string,
|
|
944
|
+
apiSecret: string,
|
|
945
|
+
host: string
|
|
946
|
+
): Promise<void> {
|
|
912
947
|
return new Promise<void>((resolve) => {
|
|
913
948
|
try {
|
|
914
949
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
@@ -917,17 +952,23 @@ export const Task = class Task<
|
|
|
917
952
|
const http = require('http')
|
|
918
953
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
919
954
|
const url = require('url')
|
|
955
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
956
|
+
const { v7: uuidv7 } = require('uuid')
|
|
920
957
|
|
|
921
|
-
const logsUrl = `${host}/api/
|
|
922
|
-
// eslint-disable-next-line no-console
|
|
923
|
-
console.log('logsUrl', logsUrl)
|
|
958
|
+
const logsUrl = `${host}/api/log-ingest`
|
|
924
959
|
const parsedUrl = url.parse(logsUrl)
|
|
925
960
|
const authToken = `${apiKey}:${apiSecret}`
|
|
926
961
|
|
|
962
|
+
// Ensure log has UUID for the new endpoint
|
|
963
|
+
const logWithUuid = {
|
|
964
|
+
...log,
|
|
965
|
+
uuid: log.uuid || uuidv7()
|
|
966
|
+
}
|
|
967
|
+
|
|
927
968
|
const postData = JSON.stringify({
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
logItem: JSON.stringify(
|
|
969
|
+
projectUuid,
|
|
970
|
+
taskUuid,
|
|
971
|
+
logItem: JSON.stringify(logWithUuid)
|
|
931
972
|
})
|
|
932
973
|
|
|
933
974
|
const options = {
|
|
@@ -947,12 +988,9 @@ export const Task = class Task<
|
|
|
947
988
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
948
989
|
const req = client.request(options, (res: any) => {
|
|
949
990
|
// eslint-disable-next-line no-console
|
|
950
|
-
console.log('Hive API response status:', res.statusCode)
|
|
951
|
-
// eslint-disable-next-line no-console
|
|
952
|
-
console.log('Hive API response headers:', res.headers)
|
|
991
|
+
console.log('Hive UUID API response status:', res.statusCode)
|
|
953
992
|
|
|
954
993
|
let responseData = ''
|
|
955
|
-
|
|
956
994
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
957
995
|
res.on('data', (chunk: any) => {
|
|
958
996
|
responseData += chunk
|
|
@@ -960,33 +998,34 @@ export const Task = class Task<
|
|
|
960
998
|
|
|
961
999
|
res.on('end', () => {
|
|
962
1000
|
// eslint-disable-next-line no-console
|
|
963
|
-
console.log('Hive API response body:', responseData)
|
|
1001
|
+
console.log('Hive UUID API response body:', responseData)
|
|
964
1002
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
965
1003
|
// eslint-disable-next-line no-console
|
|
966
|
-
console.log('Successfully sent log to Hive')
|
|
1004
|
+
console.log('Successfully sent log to Hive using UUID endpoint')
|
|
967
1005
|
} else {
|
|
968
1006
|
// eslint-disable-next-line no-console
|
|
969
|
-
console.error('Hive API error - Status:', res.statusCode, 'Body:', responseData)
|
|
1007
|
+
console.error('Hive UUID API error - Status:', res.statusCode, 'Body:', responseData)
|
|
970
1008
|
}
|
|
971
|
-
resolve()
|
|
1009
|
+
resolve()
|
|
972
1010
|
})
|
|
973
1011
|
})
|
|
974
1012
|
|
|
975
1013
|
req.on('error', (error: Error) => {
|
|
976
1014
|
// eslint-disable-next-line no-console
|
|
977
|
-
console.error('Failed to send log to Hive - Request error:', error.message)
|
|
978
|
-
resolve()
|
|
1015
|
+
console.error('Failed to send log to Hive UUID endpoint - Request error:', error.message)
|
|
1016
|
+
resolve()
|
|
979
1017
|
})
|
|
980
1018
|
|
|
981
1019
|
req.write(postData)
|
|
982
1020
|
req.end()
|
|
983
1021
|
} catch (error) {
|
|
984
1022
|
// eslint-disable-next-line no-console
|
|
985
|
-
console.error('Failed to send log to Hive:', error instanceof Error ? error.message : 'Unknown error')
|
|
986
|
-
resolve()
|
|
1023
|
+
console.error('Failed to send log to Hive UUID endpoint:', error instanceof Error ? error.message : 'Unknown error')
|
|
1024
|
+
resolve()
|
|
987
1025
|
}
|
|
988
1026
|
})
|
|
989
1027
|
}
|
|
1028
|
+
|
|
990
1029
|
}
|
|
991
1030
|
|
|
992
1031
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from '@forgehive/schema'
|
|
1
|
+
import { Schema, z } from '@forgehive/schema'
|
|
2
2
|
import { createTask, ExecutionRecord, getExecutionRecordType } from '../index'
|
|
3
3
|
|
|
4
4
|
describe('safeReplay functionality tests', () => {
|
|
@@ -16,9 +16,8 @@ describe('safeReplay functionality tests', () => {
|
|
|
16
16
|
fetchData: (ticker: string) => Promise<number>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// ToDo: Add correct type for
|
|
20
|
-
|
|
21
|
-
let schema: Schema<Record<string, any>>
|
|
19
|
+
// ToDo: Add correct type for getTickerPrice
|
|
20
|
+
let schema: Schema<{ ticker: z.ZodString }>
|
|
22
21
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
22
|
let getTickerPrice: any // Using any temporarily until we implement safeReplay
|
|
24
23
|
|
|
@@ -36,7 +36,7 @@ describe('Validation tests', () => {
|
|
|
36
36
|
} catch (e) {
|
|
37
37
|
const error = e as Error
|
|
38
38
|
|
|
39
|
-
expect(error.message).toEqual('Invalid input on: value:
|
|
39
|
+
expect(error.message).toEqual('Invalid input on: value: Invalid input: expected number, received null')
|
|
40
40
|
}
|
|
41
41
|
})
|
|
42
42
|
|
|
@@ -49,7 +49,7 @@ describe('Validation tests', () => {
|
|
|
49
49
|
const error = e as Error
|
|
50
50
|
|
|
51
51
|
// Test the error message format
|
|
52
|
-
expect(error.message).toEqual('Invalid input on: value:
|
|
52
|
+
expect(error.message).toEqual('Invalid input on: value: Invalid input: expected number, received string')
|
|
53
53
|
}
|
|
54
54
|
})
|
|
55
55
|
|
|
@@ -93,7 +93,7 @@ describe('Validation tests on param', () => {
|
|
|
93
93
|
expect('no error thrown').toBeUndefined()
|
|
94
94
|
} catch (e) {
|
|
95
95
|
const error = e as Error
|
|
96
|
-
expect(error.message).toEqual('Invalid input on: name:
|
|
96
|
+
expect(error.message).toEqual('Invalid input on: name: Invalid input: expected string, received null')
|
|
97
97
|
}
|
|
98
98
|
})
|
|
99
99
|
|
|
@@ -127,7 +127,7 @@ describe('Validation multiple values tests', () => {
|
|
|
127
127
|
expect('no error thrown').toBeUndefined()
|
|
128
128
|
} catch (e) {
|
|
129
129
|
const error = e as Error
|
|
130
|
-
expect(error.message).toEqual('Invalid input on: value:
|
|
130
|
+
expect(error.message).toEqual('Invalid input on: value: Invalid input: expected number, received null, increment: Invalid input: expected number, received undefined')
|
|
131
131
|
}
|
|
132
132
|
})
|
|
133
133
|
|
|
@@ -138,7 +138,7 @@ describe('Validation multiple values tests', () => {
|
|
|
138
138
|
expect('no error thrown').toBeUndefined()
|
|
139
139
|
} catch (e) {
|
|
140
140
|
const error = e as Error
|
|
141
|
-
expect(error.message).toEqual('Invalid input on: increment:
|
|
141
|
+
expect(error.message).toEqual('Invalid input on: increment: Invalid input: expected number, received undefined')
|
|
142
142
|
}
|
|
143
143
|
})
|
|
144
144
|
|
|
@@ -163,7 +163,11 @@ describe('Get Schema', () => {
|
|
|
163
163
|
const schema = add2.getSchema()
|
|
164
164
|
const schemaDescription = schema?.describe() ?? {}
|
|
165
165
|
|
|
166
|
-
expect(
|
|
166
|
+
expect(schemaDescription).toMatchObject({
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: { value: { type: 'number' } },
|
|
169
|
+
required: ['value']
|
|
170
|
+
})
|
|
167
171
|
})
|
|
168
172
|
|
|
169
173
|
it('Empty object as string', async () => {
|
|
@@ -190,7 +194,11 @@ describe('Set Schema', () => {
|
|
|
190
194
|
const schema = add2.getSchema()
|
|
191
195
|
const schemaDescription = schema?.describe() ?? {}
|
|
192
196
|
|
|
193
|
-
expect(
|
|
197
|
+
expect(schemaDescription).toMatchObject({
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: { value: { type: 'number' } },
|
|
200
|
+
required: ['value']
|
|
201
|
+
})
|
|
194
202
|
})
|
|
195
203
|
|
|
196
204
|
it('Empty object as string', async () => {
|
|
@@ -230,9 +238,9 @@ describe('Multiple validation errors', () => {
|
|
|
230
238
|
const error = e as Error
|
|
231
239
|
// Test that multiple errors are reported
|
|
232
240
|
expect(error.message).toContain('Invalid input on')
|
|
233
|
-
expect(error.message).toContain('name:
|
|
234
|
-
expect(error.message).toContain('age:
|
|
235
|
-
expect(error.message).toContain('email: Invalid email')
|
|
241
|
+
expect(error.message).toContain('name: Invalid input: expected string, received number')
|
|
242
|
+
expect(error.message).toContain('age: Invalid input: expected number, received string')
|
|
243
|
+
expect(error.message).toContain('email: Invalid email address')
|
|
236
244
|
}
|
|
237
245
|
})
|
|
238
246
|
})
|
|
@@ -274,7 +282,7 @@ describe('Array validation tests', () => {
|
|
|
274
282
|
expect('no error thrown').toBeUndefined()
|
|
275
283
|
} catch (e) {
|
|
276
284
|
const error = e as Error
|
|
277
|
-
expect(error.message).toEqual('Invalid input on: tags:
|
|
285
|
+
expect(error.message).toEqual('Invalid input on: tags: Invalid input: expected array, received string')
|
|
278
286
|
}
|
|
279
287
|
})
|
|
280
288
|
|
|
@@ -286,8 +294,8 @@ describe('Array validation tests', () => {
|
|
|
286
294
|
} catch (e) {
|
|
287
295
|
const error = e as Error
|
|
288
296
|
expect(error.message).toContain('Invalid input on')
|
|
289
|
-
expect(error.message).toContain('tags.1:
|
|
290
|
-
expect(error.message).toContain('tags.2:
|
|
297
|
+
expect(error.message).toContain('tags.1: Invalid input: expected string, received number')
|
|
298
|
+
expect(error.message).toContain('tags.2: Invalid input: expected string, received boolean')
|
|
291
299
|
}
|
|
292
300
|
})
|
|
293
301
|
|
|
@@ -342,7 +350,7 @@ describe('MixedRecord validation tests', () => {
|
|
|
342
350
|
expect('no error thrown').toBeUndefined()
|
|
343
351
|
} catch (e) {
|
|
344
352
|
const error = e as Error
|
|
345
|
-
expect(error.message).toEqual('Invalid input on: metadata:
|
|
353
|
+
expect(error.message).toEqual('Invalid input on: metadata: Invalid input: expected record, received string')
|
|
346
354
|
}
|
|
347
355
|
})
|
|
348
356
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"integration-enhanced-records.test.d.ts","sourceRoot":"","sources":["../../src/test/integration-enhanced-records.test.ts"],"names":[],"mappings":""}
|