@wavehouse/sdk 0.0.0-dev.h7d02b5f9a5e4 → 0.0.0-dev.h97a11a9f5a0e

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.cts CHANGED
@@ -97,9 +97,36 @@ interface TableSchema {
97
97
  columns: Column[];
98
98
  }
99
99
  type Schemas = Record<string, TableSchema>;
100
+ /**
101
+ * A per-record outcome from a batch (array / NDJSON) insert. Mirrors the
102
+ * single-object response shape plus the record's position. Exactly one of
103
+ * `ok` / `duplicate` / `error` is set.
104
+ */
105
+ interface InsertRecordResult {
106
+ /** 1-based index of the record within the submitted batch. */
107
+ index: number;
108
+ /** Set when the record was validated and published. */
109
+ ok?: boolean;
110
+ /** Set when dedup skipped the record. */
111
+ duplicate?: boolean;
112
+ /** Set (with `ok`/`duplicate` absent) when the record was rejected. */
113
+ error?: string;
114
+ }
100
115
  interface InsertResult {
116
+ /** True for a fully successful insert: a single row, or a batch with no rejected records (`failed === 0`). */
101
117
  ok: boolean;
118
+ /** Single insert only: set when dedup skipped the row. */
102
119
  duplicate?: boolean;
120
+ /** Batch insert (array / NDJSON): number of records submitted. */
121
+ total?: number;
122
+ /** Batch insert: records validated and published. */
123
+ succeeded?: number;
124
+ /** Batch insert: records rejected — see `results`. */
125
+ failed?: number;
126
+ /** Batch insert: records skipped by dedup. */
127
+ duplicates?: number;
128
+ /** Batch insert: per-record outcomes, each `{index, ok|duplicate|error}` (may be truncated for very large batches; the counts stay authoritative). */
129
+ results?: InsertRecordResult[];
103
130
  }
104
131
  interface DLQStats {
105
132
  tables: Record<string, number>;
@@ -396,6 +423,12 @@ declare class QueryBuilder<Row = Record<string, unknown>> implements PromiseLike
396
423
  }
397
424
 
398
425
  type CreateStreamFn<Row> = (table: string, opts?: StreamOptions) => StreamController<Row>;
426
+ /**
427
+ * Accepted sources for {@link TableRef.insertNDJSON}: a string, raw bytes, a
428
+ * Blob/File (browser file inputs), or a byte stream. Non-string sources are
429
+ * read fully before sending.
430
+ */
431
+ type NDJSONSource = string | Uint8Array | Blob | ReadableStream<Uint8Array>;
399
432
  /**
400
433
  * Reference to a table. NOT thenable — safe to pass around without triggering requests.
401
434
  * Use `.fetch()`, `.select()`, `.insert()`, `.schema()`, or `.stream()` to act on it.
@@ -409,10 +442,42 @@ declare class TableRef<Row = Record<string, unknown>> {
409
442
  fetch(opts?: FetchOptions): Promise<Result<Row[]>>;
410
443
  /** Start building a typed query. Returns an immutable, PromiseLike QueryBuilder. */
411
444
  select(...columns: string[]): QueryBuilder<Row>;
412
- /** Insert one or more rows into this table. */
445
+ /**
446
+ * Insert one or more rows into this table.
447
+ *
448
+ * A single object is sent as a JSON `POST /v1/ingest`. An **array** is
449
+ * serialized to NDJSON (one record per line) and sent as a single
450
+ * `application/x-ndjson` request: a bad record no longer fails or hides the
451
+ * rest of the batch — per-record outcomes come back in the result
452
+ * (`failed` / `results`), and `ok` is true only when every record succeeded.
453
+ *
454
+ * The array path sends one request regardless of size; bounded-concurrency
455
+ * chunking of very large arrays is tracked separately (#196). For NDJSON you
456
+ * already have (a file or stream), use {@link insertNDJSON}.
457
+ */
413
458
  insert(data: Partial<Row> | Partial<Row>[], opts?: {
414
459
  signal?: AbortSignal;
415
460
  }): Promise<Result<InsertResult>>;
461
+ /**
462
+ * Insert pre-formatted NDJSON (newline-delimited JSON, one record per line)
463
+ * from a string, raw bytes, a Blob/File, or a byte stream — e.g. a `.ndjson`
464
+ * file or a stream produced by another system. For in-memory rows, prefer
465
+ * {@link insert}.
466
+ *
467
+ * Non-string sources are read fully into memory before sending (the server
468
+ * streams the parse). Returns the same per-record batch summary as an array
469
+ * `insert`.
470
+ */
471
+ insertNDJSON(source: NDJSONSource, opts?: {
472
+ signal?: AbortSignal;
473
+ }): Promise<Result<InsertResult>>;
474
+ /**
475
+ * @internal Send an NDJSON body and map the server's batch response onto an
476
+ * {@link InsertResult}. A transport / whole-request failure surfaces as the
477
+ * Result error arm; a processed batch (even with rejected records) surfaces
478
+ * as the success arm with `ok = failed === 0`.
479
+ */
480
+ private _sendNDJSON;
416
481
  /** Fetch the schema for this table. */
417
482
  schema(opts?: {
418
483
  signal?: AbortSignal;
@@ -458,4 +523,4 @@ declare class WaveHouseClient<DB extends Database = Database> {
458
523
  /** Create a new WaveHouse client instance. */
459
524
  declare function createClient<DB extends Database = Database>(config: ClientConfig<DB>): WaveHouseClient<DB>;
460
525
 
461
- export { type Aggregation, type ClientConfig, type ClientOptions, type Column, DLQNamespace, type DLQStats, type Database, type FetchOptions, type FilterOp, type InsertResult, LiveQuery, type OrderClause, type ParamDef, type Pipe, PipeRef, PipesNamespace, type Policy, type PolicyFilter, PolicyNamespace, QueryBuilder, type QueryFilter, type Result, type RolePermissions, SchemaNamespace, type Schemas, StreamController, type StreamEvent, type StreamOptions, type StreamStatus, type StreamSubscriber, type StructuredQuery, SysNamespace, type TablePolicy, TableRef, type TableSchema, type TimeRange, type ValidationResult, WaveHouseClient, type WaveHouseError, createClient };
526
+ export { type Aggregation, type ClientConfig, type ClientOptions, type Column, DLQNamespace, type DLQStats, type Database, type FetchOptions, type FilterOp, type InsertRecordResult, type InsertResult, LiveQuery, type NDJSONSource, type OrderClause, type ParamDef, type Pipe, PipeRef, PipesNamespace, type Policy, type PolicyFilter, PolicyNamespace, QueryBuilder, type QueryFilter, type Result, type RolePermissions, SchemaNamespace, type Schemas, StreamController, type StreamEvent, type StreamOptions, type StreamStatus, type StreamSubscriber, type StructuredQuery, SysNamespace, type TablePolicy, TableRef, type TableSchema, type TimeRange, type ValidationResult, WaveHouseClient, type WaveHouseError, createClient };
package/dist/index.d.ts CHANGED
@@ -97,9 +97,36 @@ interface TableSchema {
97
97
  columns: Column[];
98
98
  }
99
99
  type Schemas = Record<string, TableSchema>;
100
+ /**
101
+ * A per-record outcome from a batch (array / NDJSON) insert. Mirrors the
102
+ * single-object response shape plus the record's position. Exactly one of
103
+ * `ok` / `duplicate` / `error` is set.
104
+ */
105
+ interface InsertRecordResult {
106
+ /** 1-based index of the record within the submitted batch. */
107
+ index: number;
108
+ /** Set when the record was validated and published. */
109
+ ok?: boolean;
110
+ /** Set when dedup skipped the record. */
111
+ duplicate?: boolean;
112
+ /** Set (with `ok`/`duplicate` absent) when the record was rejected. */
113
+ error?: string;
114
+ }
100
115
  interface InsertResult {
116
+ /** True for a fully successful insert: a single row, or a batch with no rejected records (`failed === 0`). */
101
117
  ok: boolean;
118
+ /** Single insert only: set when dedup skipped the row. */
102
119
  duplicate?: boolean;
120
+ /** Batch insert (array / NDJSON): number of records submitted. */
121
+ total?: number;
122
+ /** Batch insert: records validated and published. */
123
+ succeeded?: number;
124
+ /** Batch insert: records rejected — see `results`. */
125
+ failed?: number;
126
+ /** Batch insert: records skipped by dedup. */
127
+ duplicates?: number;
128
+ /** Batch insert: per-record outcomes, each `{index, ok|duplicate|error}` (may be truncated for very large batches; the counts stay authoritative). */
129
+ results?: InsertRecordResult[];
103
130
  }
104
131
  interface DLQStats {
105
132
  tables: Record<string, number>;
@@ -396,6 +423,12 @@ declare class QueryBuilder<Row = Record<string, unknown>> implements PromiseLike
396
423
  }
397
424
 
398
425
  type CreateStreamFn<Row> = (table: string, opts?: StreamOptions) => StreamController<Row>;
426
+ /**
427
+ * Accepted sources for {@link TableRef.insertNDJSON}: a string, raw bytes, a
428
+ * Blob/File (browser file inputs), or a byte stream. Non-string sources are
429
+ * read fully before sending.
430
+ */
431
+ type NDJSONSource = string | Uint8Array | Blob | ReadableStream<Uint8Array>;
399
432
  /**
400
433
  * Reference to a table. NOT thenable — safe to pass around without triggering requests.
401
434
  * Use `.fetch()`, `.select()`, `.insert()`, `.schema()`, or `.stream()` to act on it.
@@ -409,10 +442,42 @@ declare class TableRef<Row = Record<string, unknown>> {
409
442
  fetch(opts?: FetchOptions): Promise<Result<Row[]>>;
410
443
  /** Start building a typed query. Returns an immutable, PromiseLike QueryBuilder. */
411
444
  select(...columns: string[]): QueryBuilder<Row>;
412
- /** Insert one or more rows into this table. */
445
+ /**
446
+ * Insert one or more rows into this table.
447
+ *
448
+ * A single object is sent as a JSON `POST /v1/ingest`. An **array** is
449
+ * serialized to NDJSON (one record per line) and sent as a single
450
+ * `application/x-ndjson` request: a bad record no longer fails or hides the
451
+ * rest of the batch — per-record outcomes come back in the result
452
+ * (`failed` / `results`), and `ok` is true only when every record succeeded.
453
+ *
454
+ * The array path sends one request regardless of size; bounded-concurrency
455
+ * chunking of very large arrays is tracked separately (#196). For NDJSON you
456
+ * already have (a file or stream), use {@link insertNDJSON}.
457
+ */
413
458
  insert(data: Partial<Row> | Partial<Row>[], opts?: {
414
459
  signal?: AbortSignal;
415
460
  }): Promise<Result<InsertResult>>;
461
+ /**
462
+ * Insert pre-formatted NDJSON (newline-delimited JSON, one record per line)
463
+ * from a string, raw bytes, a Blob/File, or a byte stream — e.g. a `.ndjson`
464
+ * file or a stream produced by another system. For in-memory rows, prefer
465
+ * {@link insert}.
466
+ *
467
+ * Non-string sources are read fully into memory before sending (the server
468
+ * streams the parse). Returns the same per-record batch summary as an array
469
+ * `insert`.
470
+ */
471
+ insertNDJSON(source: NDJSONSource, opts?: {
472
+ signal?: AbortSignal;
473
+ }): Promise<Result<InsertResult>>;
474
+ /**
475
+ * @internal Send an NDJSON body and map the server's batch response onto an
476
+ * {@link InsertResult}. A transport / whole-request failure surfaces as the
477
+ * Result error arm; a processed batch (even with rejected records) surfaces
478
+ * as the success arm with `ok = failed === 0`.
479
+ */
480
+ private _sendNDJSON;
416
481
  /** Fetch the schema for this table. */
417
482
  schema(opts?: {
418
483
  signal?: AbortSignal;
@@ -458,4 +523,4 @@ declare class WaveHouseClient<DB extends Database = Database> {
458
523
  /** Create a new WaveHouse client instance. */
459
524
  declare function createClient<DB extends Database = Database>(config: ClientConfig<DB>): WaveHouseClient<DB>;
460
525
 
461
- export { type Aggregation, type ClientConfig, type ClientOptions, type Column, DLQNamespace, type DLQStats, type Database, type FetchOptions, type FilterOp, type InsertResult, LiveQuery, type OrderClause, type ParamDef, type Pipe, PipeRef, PipesNamespace, type Policy, type PolicyFilter, PolicyNamespace, QueryBuilder, type QueryFilter, type Result, type RolePermissions, SchemaNamespace, type Schemas, StreamController, type StreamEvent, type StreamOptions, type StreamStatus, type StreamSubscriber, type StructuredQuery, SysNamespace, type TablePolicy, TableRef, type TableSchema, type TimeRange, type ValidationResult, WaveHouseClient, type WaveHouseError, createClient };
526
+ export { type Aggregation, type ClientConfig, type ClientOptions, type Column, DLQNamespace, type DLQStats, type Database, type FetchOptions, type FilterOp, type InsertRecordResult, type InsertResult, LiveQuery, type NDJSONSource, type OrderClause, type ParamDef, type Pipe, PipeRef, PipesNamespace, type Policy, type PolicyFilter, PolicyNamespace, QueryBuilder, type QueryFilter, type Result, type RolePermissions, SchemaNamespace, type Schemas, StreamController, type StreamEvent, type StreamOptions, type StreamStatus, type StreamSubscriber, type StructuredQuery, SysNamespace, type TablePolicy, TableRef, type TableSchema, type TimeRange, type ValidationResult, WaveHouseClient, type WaveHouseError, createClient };
package/dist/index.js CHANGED
@@ -39,9 +39,10 @@ function err(error) {
39
39
  async function request(ctx, opts) {
40
40
  const url = buildURL(ctx.baseURL, opts.path, opts.params);
41
41
  const headers = {
42
- "Content-Type": "application/json",
42
+ "Content-Type": opts.contentType ?? "application/json",
43
43
  Accept: "application/json"
44
44
  };
45
+ const requestBody = opts.rawBody !== void 0 ? opts.rawBody : opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
45
46
  if (ctx.auth) {
46
47
  const token = await ctx.auth();
47
48
  if (token) {
@@ -55,7 +56,7 @@ async function request(ctx, opts) {
55
56
  const res = await fetch(url, {
56
57
  method: opts.method,
57
58
  headers,
58
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
59
+ body: requestBody,
59
60
  signal: opts.signal
60
61
  });
61
62
  if (res.ok) {
@@ -745,11 +746,11 @@ var QueryBuilder = class _QueryBuilder {
745
746
  if (error) return err(error);
746
747
  const rows = data;
747
748
  const hasMore = effectiveLimit != null && rows.length >= effectiveLimit;
748
- if (hasMore) {
749
+ if (hasMore && this._state.orderBy.length > 0) {
749
750
  const nextFn = () => this._fetchNext(rows, effectiveLimit, opts);
750
751
  return okPage(rows, true, nextFn);
751
752
  }
752
- return okPage(rows, false);
753
+ return okPage(rows, hasMore);
753
754
  }
754
755
  stream(opts) {
755
756
  const raw = this._createStream(this._state.table, opts);
@@ -792,27 +793,23 @@ var QueryBuilder = class _QueryBuilder {
792
793
  if (this._state.aggregations.length > 0) ast.aggregations = this._state.aggregations;
793
794
  if (this._state.filters.length > 0) ast.filters = this._state.filters;
794
795
  if (this._state.groupBy.length > 0) ast.group_by = this._state.groupBy;
795
- if (this._state.orderBy.length > 0) {
796
- ast.order_by = this._state.orderBy;
797
- } else if (effectiveLimit != null && this._state.aggregations.length === 0) {
798
- ast.order_by = [{ column: "received_timestamp", dir: "desc" }];
799
- }
796
+ if (this._state.orderBy.length > 0) ast.order_by = this._state.orderBy;
800
797
  if (effectiveLimit != null) ast.limit = effectiveLimit;
801
798
  if (this._state.timeRange) ast.time_range = this._state.timeRange;
802
799
  return ast;
803
800
  }
804
801
  async _fetchNext(prevRows, _limit, opts) {
805
- const orderCol = this._state.orderBy[0]?.column ?? "received_timestamp";
806
- const orderDir = this._state.orderBy[0]?.dir ?? "desc";
802
+ const cursor = this._state.orderBy[0];
803
+ if (cursor == null) return okPage([], false);
804
+ const { column: orderCol, dir: orderDir } = cursor;
807
805
  const lastRow = prevRows[prevRows.length - 1];
808
806
  const lastValue = lastRow?.[orderCol];
809
807
  if (lastValue === void 0) return okPage([], false);
810
808
  const cursorOp = orderDir === "desc" ? "lt" : "gt";
811
809
  const cursorFilter = { column: orderCol, op: cursorOp, value: lastValue };
812
- const orderBy = this._state.orderBy.length > 0 ? this._state.orderBy : [{ column: orderCol, dir: orderDir }];
813
810
  const nextBuilder = this._clone({
814
811
  filters: [...this._state.filters, cursorFilter],
815
- orderBy
812
+ orderBy: this._state.orderBy
816
813
  });
817
814
  return nextBuilder.fetch(opts);
818
815
  }
@@ -903,6 +900,12 @@ function projectColumns(row, columns) {
903
900
  }
904
901
 
905
902
  // src/table.ts
903
+ var NDJSON_CONTENT_TYPE = "application/x-ndjson";
904
+ async function ndjsonSourceToString(source) {
905
+ if (typeof source === "string") return source;
906
+ if (source instanceof Uint8Array) return new TextDecoder().decode(source);
907
+ return new Response(source).text();
908
+ }
906
909
  var TableRef = class {
907
910
  _ctx;
908
911
  _table;
@@ -931,22 +934,26 @@ var TableRef = class {
931
934
  this._createStream
932
935
  );
933
936
  }
934
- /** Insert one or more rows into this table. */
937
+ /**
938
+ * Insert one or more rows into this table.
939
+ *
940
+ * A single object is sent as a JSON `POST /v1/ingest`. An **array** is
941
+ * serialized to NDJSON (one record per line) and sent as a single
942
+ * `application/x-ndjson` request: a bad record no longer fails or hides the
943
+ * rest of the batch — per-record outcomes come back in the result
944
+ * (`failed` / `results`), and `ok` is true only when every record succeeded.
945
+ *
946
+ * The array path sends one request regardless of size; bounded-concurrency
947
+ * chunking of very large arrays is tracked separately (#196). For NDJSON you
948
+ * already have (a file or stream), use {@link insertNDJSON}.
949
+ */
935
950
  async insert(data, opts) {
936
951
  if (Array.isArray(data)) {
937
- const promises = data.map(
938
- (row) => request(this._ctx, {
939
- method: "POST",
940
- path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
941
- body: row,
942
- signal: opts?.signal
943
- })
944
- );
945
- const results = await Promise.all(promises);
946
- for (const res2 of results) {
947
- if (res2.error) return err(res2.error);
952
+ if (data.length === 0) {
953
+ return ok({ ok: true, total: 0, succeeded: 0, failed: 0, duplicates: 0, results: [] });
948
954
  }
949
- return ok({ ok: true });
955
+ const ndjson = data.map((row) => JSON.stringify(row)).join("\n");
956
+ return this._sendNDJSON(ndjson, opts);
950
957
  }
951
958
  const { data: res, error } = await request(this._ctx, {
952
959
  method: "POST",
@@ -959,6 +966,46 @@ var TableRef = class {
959
966
  if (res?.duplicate != null) result.duplicate = res.duplicate;
960
967
  return ok(result);
961
968
  }
969
+ /**
970
+ * Insert pre-formatted NDJSON (newline-delimited JSON, one record per line)
971
+ * from a string, raw bytes, a Blob/File, or a byte stream — e.g. a `.ndjson`
972
+ * file or a stream produced by another system. For in-memory rows, prefer
973
+ * {@link insert}.
974
+ *
975
+ * Non-string sources are read fully into memory before sending (the server
976
+ * streams the parse). Returns the same per-record batch summary as an array
977
+ * `insert`.
978
+ */
979
+ async insertNDJSON(source, opts) {
980
+ const ndjson = await ndjsonSourceToString(source);
981
+ return this._sendNDJSON(ndjson, opts);
982
+ }
983
+ /**
984
+ * @internal Send an NDJSON body and map the server's batch response onto an
985
+ * {@link InsertResult}. A transport / whole-request failure surfaces as the
986
+ * Result error arm; a processed batch (even with rejected records) surfaces
987
+ * as the success arm with `ok = failed === 0`.
988
+ */
989
+ async _sendNDJSON(ndjson, opts) {
990
+ const { data, error } = await request(this._ctx, {
991
+ method: "POST",
992
+ path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
993
+ rawBody: ndjson,
994
+ contentType: NDJSON_CONTENT_TYPE,
995
+ signal: opts?.signal
996
+ });
997
+ if (error) return err(error);
998
+ const r = data ?? { total: 0, succeeded: 0, failed: 0, duplicates: 0 };
999
+ const result = {
1000
+ ok: r.failed === 0,
1001
+ total: r.total,
1002
+ succeeded: r.succeeded,
1003
+ failed: r.failed,
1004
+ duplicates: r.duplicates
1005
+ };
1006
+ if (r.results && r.results.length > 0) result.results = r.results;
1007
+ return ok(result);
1008
+ }
962
1009
  /** Fetch the schema for this table. */
963
1010
  async schema(opts) {
964
1011
  const { data, error } = await request(this._ctx, {