@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/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> ? InferSchema<S> : Record<string, unknown>;
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
- // Access the underlying Zod schema and get the shape
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 isEmpty
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?.errors.map(err =>
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?.errors.map(err =>
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?.errors.map(err =>
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?.errors
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 || !projectName) {
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 required env vars for sending log to Hive:', { apiKey, apiSecret, host, projectName })
932
+ console.log('Missing project or task UUID, skipping log send')
906
933
  return
907
934
  }
908
935
 
909
- // eslint-disable-next-line no-console
910
- console.log('Sending log to Hive:', log)
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/tasks/log-ingest`
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
- projectName,
929
- taskName: process.env.HIVE_TASK_NAME || this._fn.name || 'unnamed-task',
930
- logItem: JSON.stringify(log)
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() // Resolve the promise when request completes
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() // Resolve even on error to not block the handler
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() // Resolve even on error to not block the handler
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 schema and getTickerPrice
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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: Expected number, received null')
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: Expected number, received string')
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: Expected string, received null')
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: Expected number, received null, increment: Required')
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: Required')
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(JSON.stringify(schemaDescription)).toBe('{"value":{"type":"number"}}')
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(JSON.stringify(schemaDescription)).toBe('{"value":{"type":"number"}}')
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: Expected string, received number')
234
- expect(error.message).toContain('age: Expected number, received string')
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: Expected array, received string')
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: Expected string, received number')
290
- expect(error.message).toContain('tags.2: Expected string, received boolean')
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: Expected object, received string')
353
+ expect(error.message).toEqual('Invalid input on: metadata: Invalid input: expected record, received string')
346
354
  }
347
355
  })
348
356
 
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=integration-enhanced-records.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"integration-enhanced-records.test.d.ts","sourceRoot":"","sources":["../../src/test/integration-enhanced-records.test.ts"],"names":[],"mappings":""}