@lakeql/response-transformer 0.1.7 → 1.0.1

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.mts CHANGED
@@ -1,163 +1,136 @@
1
- //#region src/types.d.ts
1
+ import { AdapterName } from "@lakeql/datasource";
2
2
  /**
3
- * Primitive type
4
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1
3
+ * Adapter-neutral, schema-aware transformation for keyed raw records.
4
+ *
5
+ * Runs after an adapter's `ResponseNormalizer` has decoded the wire protocol
6
+ * into keyed records. Walks each record against a schema, converting nested
7
+ * objects and arrays, applying field mapping, and normalizing null-ish scalar
8
+ * values. It never branches on adapter names and never decodes date/timestamp
9
+ * or timezone information — that stays adapter-owned in the normalizer.
10
+ *
11
+ * @module
5
12
  */
6
- type JSONSchema7TypeName = "string" | "number" | "integer" | "boolean" | "object" | "array" | "null";
7
13
  /**
8
- * Primitive type
9
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1
14
+ * Minimal JSON-Schema subset interpreted by {@link transformRecords}.
15
+ */
16
+ interface TransformSchema {
17
+ /** JSON-Schema type. Only `"object"` and `"array"` trigger recursion. */
18
+ type?: string;
19
+ /** Property schemas for `"object"` types, keyed by SOURCE field name. */
20
+ properties?: Record<string, TransformSchema>;
21
+ /** Item schema for `"array"` types. */
22
+ items?: TransformSchema;
23
+ }
24
+ /**
25
+ * Input for {@link transformRecords}.
10
26
  */
11
- type JSONSchema7Type = string | number | boolean | JSONSchema7Object | JSONSchema7Array | null;
12
- interface JSONSchema7Object {
13
- [key: string]: JSONSchema7Type;
27
+ interface TransformRecordsInput {
28
+ /** Already-keyed records produced by an adapter normalizer. */
29
+ records: readonly Record<string, unknown>[];
30
+ /** Schema used to interpret nested object/array fields. */
31
+ schema: TransformSchema;
32
+ /**
33
+ * Field renaming, direction SOURCE key → TARGET key.
34
+ *
35
+ * Top-level fields are typically already renamed by the query dialect via
36
+ * SQL aliases, so this mapping is primarily relevant for fields nested
37
+ * inside object/array structures where SQL aliases cannot reach.
38
+ */
39
+ fieldMapping?: Readonly<Record<string, string>>;
14
40
  }
15
- type JSONSchema7Array = JSONSchema7Type[];
16
41
  /**
17
- * Meta schema
42
+ * Transforms keyed raw records against a schema.
18
43
  *
19
- * Recommended values:
20
- * - 'http://json-schema.org/schema#'
21
- * - 'http://json-schema.org/hyper-schema#'
22
- * - 'http://json-schema.org/draft-07/schema#'
23
- * - 'http://json-schema.org/draft-07/hyper-schema#'
44
+ * @param input - Records, schema, and optional field mapping.
45
+ * @returns A new array of transformed records.
46
+ */
47
+ export declare function transformRecords(input: TransformRecordsInput): Record<string, unknown>[];
48
+ /**
49
+ * Neutral input shape consumed by {@link ResponseNormalizer.normalize}.
24
50
  *
25
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-5
51
+ * Contains the raw query response rows and metadata needed to convert
52
+ * backend-specific values into the shared {@link NormalizedResponse} shape.
26
53
  */
27
- type JSONSchema7Version = string;
54
+ interface ResponseNormalizationInput {
55
+ /** Raw rows returned by the data source query. */
56
+ response: readonly unknown[];
57
+ /** Field names selected by the caller. */
58
+ selectFields: readonly string[];
59
+ /** Mapping from source field names to target field names. */
60
+ transformFields: Readonly<Record<string, string>>;
61
+ /** Field names that carry date/timestamp values. */
62
+ dateFields: readonly string[];
63
+ /** Backend-specific schema used for value interpretation. */
64
+ schema: unknown;
65
+ }
28
66
  /**
29
- * JSON Schema v7
30
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01
67
+ * Result shape produced by {@link ResponseNormalizer.normalize}.
68
+ *
69
+ * @template T - Shape of each record in the returned data array.
31
70
  */
32
- type JSONSchema7Definition = JSONSchema7 | boolean;
33
- interface JSONSchema7 {
34
- $id?: string | undefined;
35
- $ref?: string | undefined;
36
- $schema?: JSONSchema7Version | undefined;
37
- $comment?: string | undefined;
38
- /**
39
- * @see https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00#section-8.2.4
40
- * @see https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#appendix-A
41
- */
42
- $defs?: Record<string, JSONSchema7Definition> | undefined;
43
- /**
44
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1
45
- */
46
- type?: JSONSchema7TypeName | JSONSchema7TypeName[] | undefined;
47
- enum?: JSONSchema7Type[] | undefined;
48
- const?: JSONSchema7Type | undefined;
49
- /**
50
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.2
51
- */
52
- multipleOf?: number | undefined;
53
- maximum?: number | undefined;
54
- exclusiveMaximum?: number | undefined;
55
- minimum?: number | undefined;
56
- exclusiveMinimum?: number | undefined;
57
- /**
58
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.3
59
- */
60
- maxLength?: number | undefined;
61
- minLength?: number | undefined;
62
- pattern?: string | undefined;
63
- /**
64
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4
65
- */
66
- items?: JSONSchema7Definition | JSONSchema7Definition[] | undefined;
67
- additionalItems?: JSONSchema7Definition | undefined;
68
- maxItems?: number | undefined;
69
- minItems?: number | undefined;
70
- uniqueItems?: boolean | undefined;
71
- contains?: JSONSchema7Definition | undefined;
72
- /**
73
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.5
74
- */
75
- maxProperties?: number | undefined;
76
- minProperties?: number | undefined;
77
- required?: string[] | undefined;
78
- properties?: Record<string, JSONSchema7Definition> | undefined;
79
- patternProperties?: Record<string, JSONSchema7Definition> | undefined;
80
- additionalProperties?: JSONSchema7Definition | undefined;
81
- dependencies?: Record<string, JSONSchema7Definition | string[]> | undefined;
82
- propertyNames?: JSONSchema7Definition | undefined;
83
- /**
84
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.6
85
- */
86
- if?: JSONSchema7Definition | undefined;
87
- then?: JSONSchema7Definition | undefined;
88
- else?: JSONSchema7Definition | undefined;
89
- /**
90
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.7
91
- */
92
- allOf?: JSONSchema7Definition[] | undefined;
93
- anyOf?: JSONSchema7Definition[] | undefined;
94
- oneOf?: JSONSchema7Definition[] | undefined;
95
- not?: JSONSchema7Definition | undefined;
71
+ interface NormalizedResponse<T> {
72
+ /** Total number of records matching the query, before paging. */
73
+ totalCount: number;
74
+ /** Transformed records for the current page. */
75
+ data: T[];
76
+ }
77
+ /**
78
+ * Adapter-neutral contract for converting raw query response rows into a
79
+ * {@link NormalizedResponse}.
80
+ *
81
+ * Concrete adapters implement this interface to own backend-specific value
82
+ * conversion, schema interpretation, and total-count extraction. The shared
83
+ * SQL read strategy passes neutral input through unchanged and never branches
84
+ * on adapter names.
85
+ */
86
+ interface ResponseNormalizer {
87
+ /** Open identifier of the adapter that owns this normalizer. */
88
+ readonly adapterName: AdapterName;
96
89
  /**
97
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7
90
+ * Convert raw response rows into a normalized result.
91
+ *
92
+ * @template T - Shape of each record in the returned data array.
93
+ * @param input - Neutral normalization parameters.
94
+ * @returns Normalized response containing total count and typed data.
98
95
  */
99
- format?: string | undefined;
96
+ normalize: <T>(input: ResponseNormalizationInput) => NormalizedResponse<T>;
97
+ }
98
+ /**
99
+ * Options for {@link createCteResponseNormalizer}.
100
+ */
101
+ interface CteResponseNormalizerOptions {
102
+ /** Open identifier of the adapter that owns this normalizer. */
103
+ adapterName: AdapterName;
100
104
  /**
101
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-8
105
+ * Predicate deciding whether a non-null sentinel value marks a real data
106
+ * row. This is adapter-specific and MUST stay injected: Trino accepts only
107
+ * the boolean `true`, while Fabric's TDS driver returns `1`. Widening one
108
+ * adapter's predicate to the other's would change validation behavior.
102
109
  */
103
- contentMediaType?: string | undefined;
104
- contentEncoding?: string | undefined;
110
+ isPresentSentinel: (value: unknown) => boolean;
105
111
  /**
106
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9
112
+ * Human-readable description of the accepted sentinel values, used in the
113
+ * error thrown for an invalid sentinel (e.g. `"true"` or `"true/1"`).
107
114
  */
108
- definitions?: Record<string, JSONSchema7Definition> | undefined;
115
+ acceptedSentinelDescription: string;
109
116
  /**
110
- * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-10
117
+ * Strict plain-object guard. Injected because each adapter already ships its
118
+ * own (identical) implementation; passing it keeps this package free of a
119
+ * dependency on either adapter's `sql-helpers`.
111
120
  */
112
- title?: string | undefined;
113
- description?: string | undefined;
114
- default?: JSONSchema7Type | undefined;
115
- readOnly?: boolean | undefined;
116
- writeOnly?: boolean | undefined;
117
- examples?: JSONSchema7Type | undefined;
118
- }
119
- //#endregion
120
- //#region src/index.d.ts
121
- interface TransformInput {
122
- data: Record<string, unknown>;
123
- definition: JSONSchema7;
124
- transformFields?: Record<string, string>;
125
- dateFields?: string[];
126
- utcDates?: string[];
121
+ isPlainObject: (value: unknown) => boolean;
127
122
  }
128
- interface TransformArrayInput {
129
- data: Record<string, unknown>[];
130
- definition: JSONSchema7;
131
- transformFields?: Record<string, string>;
132
- dateFields?: string[];
133
- }
134
- declare const transform: ({ data, definition, transformFields, dateFields, utcDates }: TransformInput) => Record<string, unknown>;
135
- declare const transformArray: ({ data, definition, transformFields, dateFields }: TransformArrayInput) => Record<string, unknown>[];
136
- declare const transformObject: ({ data, definition, transformFields, dateFields }: TransformInput) => Record<string, unknown>;
137
123
  /**
138
- * Parameters for convertTrinoResponse.
139
- */
140
- interface ConvertTrinoResponseProps {
141
- /** The field names to use as object keys. */
142
- keys: string[];
143
- /** The positional values from the Trino response row. */
144
- values: unknown[];
145
- }
146
- /**
147
- * Converts the trino response element ( array of unknown ) to an object
148
- * based on the given keys. This function expects that
149
- * amount of keys matches the amount of value elements
124
+ * Creates a {@link ResponseNormalizer} for the shared CTE query shape.
125
+ *
126
+ * The returned normalizer extracts and validates `total_records`, drops the
127
+ * synthetic count-only row and the internal metadata columns, converts date
128
+ * fields from seconds to milliseconds, and preserves all other values. The
129
+ * only adapter-specific behavior is the sentinel predicate.
150
130
  *
151
- * @example
152
- * const keys = ["fieldName1", "fieldName2"]
153
- * const values = ["value1", 42]
154
- * const convert = convertTrinoResponse({keys, values})
155
- * // Result:
156
- * //[{"fieldName1": "value1", "fieldName2": 42}]
157
- * @param keys string[]
158
- * @param values unknown[]
159
- * @returns Record<string, unknown>
131
+ * @param options - Adapter name, sentinel predicate and description, and the
132
+ * plain-object guard.
133
+ * @returns A normalizer implementing the neutral {@link ResponseNormalizer}.
160
134
  */
161
- declare function convertTrinoResponse<T = Record<string, unknown>>({ keys, values }: ConvertTrinoResponseProps): T;
162
- //#endregion
163
- export { ConvertTrinoResponseProps, TransformArrayInput, TransformInput, convertTrinoResponse, transform, transformArray, transformObject };
135
+ export declare function createCteResponseNormalizer(options: CteResponseNormalizerOptions): ResponseNormalizer;
136
+ export type { CteResponseNormalizerOptions, NormalizedResponse, ResponseNormalizationInput, ResponseNormalizer, TransformRecordsInput, TransformSchema };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{format as e}from"@formkit/tempo";const t=({data:t,definition:i,transformFields:a,dateFields:o=[],utcDates:s=[]})=>{let c={};for(let[l,u]of Object.entries(t)){let t=a?.[l]??l,d=i.properties?.[l];if(d){if(d.type!==`object`&&d.type!==`array`)if(o.includes(l)){let n=u&&u*1e3;s.includes(l)?c[t]=`${e({date:new Date(n),format:`YYYY-MM-DDTHH:mm:ss`,tz:`Europe/Berlin`})}Z`:c[t]=n}else c[t]=u??null;d.type===`object`&&(c[t]=r({data:u,dateFields:o,definition:d,transformFields:a})),d.type===`array`&&(c[t]=n({data:u,dateFields:o,definition:d.items,transformFields:a}))}}return c},n=({data:e,definition:t,transformFields:n,dateFields:i})=>t.type===`object`?e.map(e=>r({data:e,dateFields:i,definition:t,transformFields:n})):e,r=({data:e,definition:t,transformFields:i,dateFields:a=[]})=>{let o={},s=Object.keys(t.properties??{}),c=Array.isArray(e)?s.map((t,n)=>[t,e[n]]):s.map(e=>[e,null]),l={};for(let[e,t]of c)l[e]=t;let u=l;for(let e of s){let s=i?.[e]??e,c=t.properties?.[e];c&&(c.type!==`array`&&c.type!==`object`&&(a.includes(s)?o[s]=u[e]&&u[e]*1e3:o[s]=u[e]??null),c.type===`object`&&(o[s]=r({data:u[e],dateFields:a,definition:c.properties,transformFields:i})),c.type===`array`&&(o[s]=n({data:u[e],dateFields:a,definition:c.items,transformFields:i})))}return o};function i({keys:e,values:t}){let n=[];for(let[r,i]of t.entries())n.push([e[r],i]);return Object.fromEntries(n)}export{i as convertTrinoResponse,t as transform,n as transformArray,r as transformObject};
1
+ function e(e){return e===`object`||e===`array`}function t(e,t,r){let i=t.properties??{},a={};for(let t of Object.keys(i)){let o=i[t];if(o===void 0)continue;let s=r[t]??t;a[s]=n(e[t],o,r)}return a}function n(r,i,a){if(i.type===`object`)return r==null?null:typeof r!=`object`||Array.isArray(r)?r:t(r,i,a);if(i.type===`array`){if(r==null)return null;if(!Array.isArray(r))return r;let t=i.items;return t===void 0||!e(t.type)?r:r.map(e=>n(e,t,a))}return r??null}function r(e){let{records:n,schema:r,fieldMapping:i={}}=e;return n.map(e=>t(e,r,i))}const i=`total_records`,a=`__lakeql_record_present`;function o(e){if(e==null)throw Error(`Malformed count metadata: total_records is absent or null`);if(typeof e!=`number`)throw TypeError(`Malformed count metadata: total_records must be a number, got ${typeof e}`);if(!Number.isFinite(e))throw TypeError(`Malformed count metadata: total_records must be finite, got ${String(e)}`);if(e<0)throw RangeError(`Malformed count metadata: total_records must be non-negative, got ${e}`);if(!Number.isInteger(e))throw TypeError(`Malformed count metadata: total_records must be an integer, got ${e}`);return e}function s(e,t){if(e===null)return null;if(typeof e!=`number`)throw TypeError(`Invalid date field "${t}": expected a finite number, got ${typeof e}`);if(!Number.isFinite(e))throw TypeError(`Invalid date field "${t}": expected a finite number, got ${String(e)}`);return e*1e3}function c(e,t,n){if(!n(e))throw Error(`Malformed adapter response: row at index ${t} must be a non-null plain object, got ${e===null?`null`:typeof e}`);return e}function l(e){let{adapterName:t,isPresentSentinel:n,acceptedSentinelDescription:r,isPlainObject:l}=e;return{adapterName:t,normalize(e){let{response:t,dateFields:u}=e;if(t.length===0)return{totalCount:0,data:[]};let d=o(c(t[0],0,l)[i]),f=new Set(u),p=[],m=0;for(let e of t){let t=c(e,m,l);m+=1;let o=t[a];if(o===null)continue;if(!n(o))throw Error(`Malformed adapter response: "${a}" must be ${r} for data rows or null for synthetic rows, got ${typeof o==`string`?`"${o}"`:String(o)}`);let u={};for(let e of Object.keys(t))e!==i&&e!==a&&(u[e]=f.has(e)?s(t[e],e):t[e]);p.push(u)}return{totalCount:d,data:p}}}}export{l as createCteResponseNormalizer,r as transformRecords};
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@lakeql/response-transformer",
3
- "version": "0.1.7",
4
- "description": "Transforms Trino array responses into typed JavaScript objects",
3
+ "version": "1.0.1",
4
+ "description": "Adapter-neutral, schema-aware transformation of keyed records for LakeQL",
5
5
  "keywords": [
6
6
  "lakeql",
7
7
  "mapping",
8
8
  "response",
9
- "transform",
10
- "trino"
9
+ "schema",
10
+ "transform"
11
11
  ],
12
12
  "homepage": "https://lakeql.dev",
13
13
  "license": "Apache-2.0",
@@ -38,19 +38,19 @@
38
38
  "import": "./dist/index.mjs"
39
39
  }
40
40
  },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
41
44
  "dependencies": {
42
- "@formkit/tempo": "1.1.0"
45
+ "@lakeql/datasource": "1.0.1"
43
46
  },
44
47
  "devDependencies": {
45
- "@faker-js/faker": "10.5.0",
46
- "@types/node": "24.13.3",
47
- "@valibot/to-json-schema": "1.7.1",
48
- "tsdown": "0.22.13",
49
- "tsx": "4.23.1",
48
+ "@lakeql/tsconfig": "0.1.0",
49
+ "@lakeql/vitest-config": "0.1.0",
50
+ "@types/node": "24.19.2",
51
+ "tsdown": "0.23.7",
50
52
  "typescript": "6.0.3",
51
- "valibot": "1.4.2",
52
- "vitest": "4.1.10",
53
- "@lakeql/tsconfig": "0.1.0"
53
+ "vitest": "5.1.0"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown",
@@ -58,6 +58,7 @@
58
58
  "clean:cache": "git clean -xdf .cache .turbo",
59
59
  "clean:dist": "git clean -xdf dist",
60
60
  "dev": "tsdown --watch",
61
+ "test": "vitest run",
61
62
  "typecheck": "tsc --noEmit"
62
63
  }
63
64
  }