@telorun/sdk 0.73.0 → 0.74.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.
@@ -39,4 +39,25 @@ export declare function isBigIntJsonEnabled(): boolean;
39
39
  * Everything that wants exact digits on the wire needs no replacer at all.
40
40
  */
41
41
  export declare function bigIntAt(holder: unknown, key: string): bigint | undefined;
42
+ /**
43
+ * A declared-integer input read as a JS number, whichever representation the
44
+ * call site produced.
45
+ *
46
+ * A CEL integer is an int64 — a BigInt — and the kernel normalizes a declared
47
+ * `type: integer` OUTPUT to that form, so one resource's result reaching another
48
+ * resource's input arrives as a BigInt while a YAML literal at the same slot
49
+ * arrives as a plain number. A controller that reads such an input with
50
+ * `Number.isInteger(...)` or plain arithmetic therefore works for one call site
51
+ * and throws `Cannot mix BigInt and other types` for the other. Inputs are
52
+ * deliberately NOT normalized (that would change the authoring surface of every
53
+ * module rather than repair a false declaration), so this is how a controller
54
+ * reads one.
55
+ *
56
+ * Returns `undefined` for anything that is not an integer in either
57
+ * representation — including a BigInt too large for a double, since silently
58
+ * rounding it would be the precision loss int64 support exists to remove — so a
59
+ * caller's own "must be a non-negative integer" check still rejects what it
60
+ * should.
61
+ */
62
+ export declare function integerInput(value: unknown): number | undefined;
42
63
  //# sourceMappingURL=bigint-json.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bigint-json.d.ts","sourceRoot":"","sources":["../src/bigint-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;uCAGuC;AACvC,eAAO,MAAM,yBAAyB,QAAgB,CAAC;AAEvD,oFAAoF;AACpF,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzE"}
1
+ {"version":3,"file":"bigint-json.d.ts","sourceRoot":"","sources":["../src/bigint-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;uCAGuC;AACvC,eAAO,MAAM,yBAAyB,QAAgB,CAAC;AAEvD,oFAAoF;AACpF,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAK/D"}
@@ -45,3 +45,31 @@ export function bigIntAt(holder, key) {
45
45
  const source = holder?.[key];
46
46
  return typeof source === "bigint" ? source : undefined;
47
47
  }
48
+ /**
49
+ * A declared-integer input read as a JS number, whichever representation the
50
+ * call site produced.
51
+ *
52
+ * A CEL integer is an int64 — a BigInt — and the kernel normalizes a declared
53
+ * `type: integer` OUTPUT to that form, so one resource's result reaching another
54
+ * resource's input arrives as a BigInt while a YAML literal at the same slot
55
+ * arrives as a plain number. A controller that reads such an input with
56
+ * `Number.isInteger(...)` or plain arithmetic therefore works for one call site
57
+ * and throws `Cannot mix BigInt and other types` for the other. Inputs are
58
+ * deliberately NOT normalized (that would change the authoring surface of every
59
+ * module rather than repair a false declaration), so this is how a controller
60
+ * reads one.
61
+ *
62
+ * Returns `undefined` for anything that is not an integer in either
63
+ * representation — including a BigInt too large for a double, since silently
64
+ * rounding it would be the precision loss int64 support exists to remove — so a
65
+ * caller's own "must be a non-negative integer" check still rejects what it
66
+ * should.
67
+ */
68
+ export function integerInput(value) {
69
+ if (typeof value === "number")
70
+ return Number.isInteger(value) ? value : undefined;
71
+ if (typeof value !== "bigint")
72
+ return undefined;
73
+ const asNumber = Number(value);
74
+ return Number.isSafeInteger(asNumber) ? asNumber : undefined;
75
+ }
@@ -38,6 +38,12 @@ export type ValueTypeRepresentation = "json" | "instance";
38
38
  * second parameter can be added without a migration. */
39
39
  export interface ValueTypeParameter {
40
40
  readonly name: string;
41
+ /** This parameter's argument is what ITERATING a value of the type yields.
42
+ * Declared here so "what is the element of this collection" is answered by
43
+ * the vocabulary rather than by a consumer that knows one type's name — the
44
+ * same reason `live` is a field and not a check against `Telo.Stream`. At
45
+ * most one parameter per entry may carry it. */
46
+ readonly element?: boolean;
41
47
  readonly description?: string;
42
48
  }
43
49
  /** One value type, exactly as its entry file declares it. */
@@ -124,6 +130,17 @@ export declare function isLiveSlot(schema: unknown): boolean;
124
130
  /** True when the node declares a type represented as a runtime instance —
125
131
  * the values no manifest literal can ever be. */
126
132
  export declare function isInstanceSlot(schema: unknown): boolean;
133
+ /**
134
+ * The schema of what iterating a value at this slot yields, or undefined when
135
+ * the slot declares no value type, or one with no element parameter.
136
+ *
137
+ * The whole point of reading it from the entry is that no consumer names a type:
138
+ * a future iterable value type is covered by declaring `element` on its own
139
+ * parameter, with nothing to change here or in the analyzer. An element
140
+ * parameter left unsupplied means *any*, exactly as every other omitted argument
141
+ * does, so an unparameterized use degrades to permissive rather than to nothing.
142
+ */
143
+ export declare function elementSchemaOf(schema: unknown): unknown | undefined;
127
144
  /** The binding row for a schema node's declared type, or undefined when it
128
145
  * declares none / declares a `json` one. */
129
146
  export declare function bindingOf(schema: unknown): ValueTypeBinding | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"value-type.d.ts","sourceRoot":"","sources":["../src/value-type.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"value-type.d.ts","sourceRoot":"","sources":["../src/value-type.ts"],"names":[],"mappings":"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"}
@@ -80,20 +80,34 @@ function readParameters(file, raw) {
80
80
  return [];
81
81
  if (!Array.isArray(raw))
82
82
  throw new ValueTypeEntryError(file, "'parameters' must be a sequence");
83
- return raw.map((entry, i) => {
83
+ const params = raw.map((entry, i) => {
84
84
  if (!isPlainObject(entry)) {
85
85
  throw new ValueTypeEntryError(file, `parameters[${i}] must be a mapping`);
86
86
  }
87
87
  for (const key of Object.keys(entry)) {
88
- if (key !== "name" && key !== "description") {
88
+ if (key !== "name" && key !== "description" && key !== "element") {
89
89
  throw new ValueTypeEntryError(file, `parameters[${i}] has no key '${key}'`);
90
90
  }
91
91
  }
92
+ if (entry.element !== undefined && typeof entry.element !== "boolean") {
93
+ throw new ValueTypeEntryError(file, `parameters[${i}].element must be a boolean when present`);
94
+ }
92
95
  const name = requireString(file, entry, "name");
93
- return entry.description === undefined
94
- ? { name }
95
- : { name, description: requireString(file, entry, "description") };
96
+ return {
97
+ name,
98
+ ...(entry.element === true ? { element: true } : {}),
99
+ ...(entry.description === undefined
100
+ ? {}
101
+ : { description: requireString(file, entry, "description") }),
102
+ };
96
103
  });
104
+ // Two element parameters would make "the element of this value" ambiguous, and
105
+ // the reader is the only place that can refuse it — every consumer takes the
106
+ // first match and would silently pick one.
107
+ if (params.filter((p) => p.element).length > 1) {
108
+ throw new ValueTypeEntryError(file, "at most one parameter may declare 'element'");
109
+ }
110
+ return params;
97
111
  }
98
112
  /**
99
113
  * Read one entry file's parsed data.
@@ -242,6 +256,23 @@ export function isLiveSlot(schema) {
242
256
  export function isInstanceSlot(schema) {
243
257
  return valueTypeOf(schema)?.representation === "instance";
244
258
  }
259
+ /**
260
+ * The schema of what iterating a value at this slot yields, or undefined when
261
+ * the slot declares no value type, or one with no element parameter.
262
+ *
263
+ * The whole point of reading it from the entry is that no consumer names a type:
264
+ * a future iterable value type is covered by declaring `element` on its own
265
+ * parameter, with nothing to change here or in the analyzer. An element
266
+ * parameter left unsupplied means *any*, exactly as every other omitted argument
267
+ * does, so an unparameterized use degrades to permissive rather than to nothing.
268
+ */
269
+ export function elementSchemaOf(schema) {
270
+ const slot = readValueTypeSlot(schema);
271
+ const parameter = slot?.entry?.parameters.find((p) => p.element);
272
+ if (!parameter)
273
+ return undefined;
274
+ return slot.args[parameter.name] ?? {};
275
+ }
245
276
  /** The binding row for a schema node's declared type, or undefined when it
246
277
  * declares none / declares a `json` one. */
247
278
  export function bindingOf(schema) {
@@ -7,6 +7,7 @@
7
7
  "parameters": [
8
8
  {
9
9
  "name": "of",
10
+ "element": true,
10
11
  "description": "The element the stream yields. Any schema node — an inline shape, a value type, a `!ref` to a named shape, or another parameterized type. Omitted means any element."
11
12
  }
12
13
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/sdk",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "description": "Telo SDK - Public API for Telo module authors.",
5
5
  "keywords": [
6
6
  "telo",
@@ -49,3 +49,30 @@ export function bigIntAt(holder: unknown, key: string): bigint | undefined {
49
49
  const source = (holder as Record<string, unknown> | null | undefined)?.[key];
50
50
  return typeof source === "bigint" ? source : undefined;
51
51
  }
52
+
53
+ /**
54
+ * A declared-integer input read as a JS number, whichever representation the
55
+ * call site produced.
56
+ *
57
+ * A CEL integer is an int64 — a BigInt — and the kernel normalizes a declared
58
+ * `type: integer` OUTPUT to that form, so one resource's result reaching another
59
+ * resource's input arrives as a BigInt while a YAML literal at the same slot
60
+ * arrives as a plain number. A controller that reads such an input with
61
+ * `Number.isInteger(...)` or plain arithmetic therefore works for one call site
62
+ * and throws `Cannot mix BigInt and other types` for the other. Inputs are
63
+ * deliberately NOT normalized (that would change the authoring surface of every
64
+ * module rather than repair a false declaration), so this is how a controller
65
+ * reads one.
66
+ *
67
+ * Returns `undefined` for anything that is not an integer in either
68
+ * representation — including a BigInt too large for a double, since silently
69
+ * rounding it would be the precision loss int64 support exists to remove — so a
70
+ * caller's own "must be a non-negative integer" check still rejects what it
71
+ * should.
72
+ */
73
+ export function integerInput(value: unknown): number | undefined {
74
+ if (typeof value === "number") return Number.isInteger(value) ? value : undefined;
75
+ if (typeof value !== "bigint") return undefined;
76
+ const asNumber = Number(value);
77
+ return Number.isSafeInteger(asNumber) ? asNumber : undefined;
78
+ }
package/src/value-type.ts CHANGED
@@ -44,6 +44,12 @@ export type ValueTypeRepresentation = "json" | "instance";
44
44
  * second parameter can be added without a migration. */
45
45
  export interface ValueTypeParameter {
46
46
  readonly name: string;
47
+ /** This parameter's argument is what ITERATING a value of the type yields.
48
+ * Declared here so "what is the element of this collection" is answered by
49
+ * the vocabulary rather than by a consumer that knows one type's name — the
50
+ * same reason `live` is a field and not a check against `Telo.Stream`. At
51
+ * most one parameter per entry may carry it. */
52
+ readonly element?: boolean;
47
53
  readonly description?: string;
48
54
  }
49
55
 
@@ -136,20 +142,34 @@ function requireString(file: string, node: Record<string, unknown>, key: string)
136
142
  function readParameters(file: string, raw: unknown): ValueTypeParameter[] {
137
143
  if (raw === undefined) return [];
138
144
  if (!Array.isArray(raw)) throw new ValueTypeEntryError(file, "'parameters' must be a sequence");
139
- return raw.map((entry, i) => {
145
+ const params = raw.map((entry, i) => {
140
146
  if (!isPlainObject(entry)) {
141
147
  throw new ValueTypeEntryError(file, `parameters[${i}] must be a mapping`);
142
148
  }
143
149
  for (const key of Object.keys(entry)) {
144
- if (key !== "name" && key !== "description") {
150
+ if (key !== "name" && key !== "description" && key !== "element") {
145
151
  throw new ValueTypeEntryError(file, `parameters[${i}] has no key '${key}'`);
146
152
  }
147
153
  }
154
+ if (entry.element !== undefined && typeof entry.element !== "boolean") {
155
+ throw new ValueTypeEntryError(file, `parameters[${i}].element must be a boolean when present`);
156
+ }
148
157
  const name = requireString(file, entry, "name");
149
- return entry.description === undefined
150
- ? { name }
151
- : { name, description: requireString(file, entry, "description") };
158
+ return {
159
+ name,
160
+ ...(entry.element === true ? { element: true as const } : {}),
161
+ ...(entry.description === undefined
162
+ ? {}
163
+ : { description: requireString(file, entry, "description") }),
164
+ };
152
165
  });
166
+ // Two element parameters would make "the element of this value" ambiguous, and
167
+ // the reader is the only place that can refuse it — every consumer takes the
168
+ // first match and would silently pick one.
169
+ if (params.filter((p) => p.element).length > 1) {
170
+ throw new ValueTypeEntryError(file, "at most one parameter may declare 'element'");
171
+ }
172
+ return params;
153
173
  }
154
174
 
155
175
  /**
@@ -334,6 +354,23 @@ export function isInstanceSlot(schema: unknown): boolean {
334
354
  return valueTypeOf(schema)?.representation === "instance";
335
355
  }
336
356
 
357
+ /**
358
+ * The schema of what iterating a value at this slot yields, or undefined when
359
+ * the slot declares no value type, or one with no element parameter.
360
+ *
361
+ * The whole point of reading it from the entry is that no consumer names a type:
362
+ * a future iterable value type is covered by declaring `element` on its own
363
+ * parameter, with nothing to change here or in the analyzer. An element
364
+ * parameter left unsupplied means *any*, exactly as every other omitted argument
365
+ * does, so an unparameterized use degrades to permissive rather than to nothing.
366
+ */
367
+ export function elementSchemaOf(schema: unknown): unknown | undefined {
368
+ const slot = readValueTypeSlot(schema);
369
+ const parameter = slot?.entry?.parameters.find((p) => p.element);
370
+ if (!parameter) return undefined;
371
+ return slot!.args[parameter.name] ?? {};
372
+ }
373
+
337
374
  /** The binding row for a schema node's declared type, or undefined when it
338
375
  * declares none / declares a `json` one. */
339
376
  export function bindingOf(schema: unknown): ValueTypeBinding | undefined {
@@ -7,6 +7,7 @@
7
7
  "parameters": [
8
8
  {
9
9
  "name": "of",
10
+ "element": true,
10
11
  "description": "The element the stream yields. Any schema node — an inline shape, a value type, a `!ref` to a named shape, or another parameterized type. Omitted means any element."
11
12
  }
12
13
  ],