@venizia/ignis-connectors 0.2.0-39 → 0.2.0-40

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.
@@ -0,0 +1,62 @@
1
+ import type { TFilter } from '@venizia/ignis-filter';
2
+ /** What a caller hands the resolver seam: the token and, optionally, which provider issued it. */
3
+ export interface IAuthToken {
4
+ value: string;
5
+ type?: string;
6
+ provider?: string;
7
+ }
8
+ /**
9
+ * Where the token comes from when the datasource was not given one outright.
10
+ *
11
+ * A seam, not a guard: a browser resolver reads storage, a server resolver reads a secret, and
12
+ * neither is baked into the transport. Returning `undefined` means "no token", which is different
13
+ * from an empty one.
14
+ */
15
+ export type TAuthTokenResolver = () => IAuthToken | undefined;
16
+ /**
17
+ * Every shape `Headers` accepts. Not `Record<string, string>`: header names are case-INSENSITIVE,
18
+ * so a plain object lets two spellings of one name coexist, and merging them APPENDS rather than
19
+ * overrides - `{ 'x-request-count': 'false', 'X-Request-Count': 'true' }` reaches the wire as
20
+ * `x-request-count: "false, true"`, which parses as neither.
21
+ */
22
+ export type THttpHeaders = Headers | Array<[string, string]> | Record<string, string>;
23
+ export interface IHttpDataSourceSettings {
24
+ baseUrl: string;
25
+ /** Sent on every request, merged through `Headers` so a caller value overrides a default. */
26
+ headers?: THttpHeaders;
27
+ /** A fixed token. Consulted BEFORE `authTokenResolver` - an explicit value is never overridden by a lookup. */
28
+ authToken?: IAuthToken;
29
+ authTokenResolver?: TAuthTokenResolver;
30
+ /**
31
+ * Runs on a 401, and its ANSWER decides: `true` retries the request once, `false` lets the 401
32
+ * stand. Absent means no retry at all.
33
+ *
34
+ * A hook rather than a boolean, because "recover from a 401" is the host's policy, not the
35
+ * transport's: one host refreshes a token then retries, another logs the user out, a third does
36
+ * both in an order only it knows. A boolean could only mean "ask the resolver again", which reads
37
+ * like recovery and is not - someone would wire it expecting a refresh and get a silent loop.
38
+ *
39
+ * The transport's part is the seam and the single retry. Whatever the hook does to make the next
40
+ * attempt work - refresh, re-login, nothing - happens before it answers, and the retry re-resolves
41
+ * the token afterwards.
42
+ */
43
+ onUnauthorized?: () => Promise<boolean> | boolean;
44
+ }
45
+ /**
46
+ * One response, with the total kept apart from the rows.
47
+ *
48
+ * `hasRange` is the whole point: a list with no `Content-Range` has NO total, which is a different
49
+ * answer from "the total equals this page". Folding the two is how a count of 7000 reports 1 and
50
+ * every screen reading it looks healthy.
51
+ */
52
+ export interface IHttpReadResult<R> {
53
+ data: R;
54
+ hasRange: boolean;
55
+ total?: number;
56
+ skip?: number;
57
+ dataLength: number;
58
+ }
59
+ export type THttpQuery<E extends object> = {
60
+ filter?: TFilter<E>;
61
+ };
62
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/http/common/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,kGAAkG;AAClG,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,UAAU,GAAG,SAAS,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACtF,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAEhB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,+GAA+G;IAC/G,SAAS,CAAC,EAAE,UAAU,CAAC;IAEvB,iBAAiB,CAAC,EAAE,kBAAkB,CAAC;IAEvC;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACnD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/http/common/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import { AbstractDataSource } from '@venizia/ignis-kernel/repository';
2
+ import { NodeFetchNetworkRequest } from '@venizia/ignis-helpers/core';
3
+ import type { IAuthToken, IHttpDataSourceSettings, IHttpReadResult } from './common/types';
4
+ /**
5
+ * A datasource whose transport is HTTP.
6
+ *
7
+ * A repository talks to a datasource, and `AbstractDataSource` is engine-neutral - its only required
8
+ * member is `configure()`. So "a request to another IGNIS server" is a datasource in the same sense
9
+ * Drizzle-over-Postgres is one: same role, different transport, and the same
10
+ * `@venizia/ignis-filter` vocabulary on the way in.
11
+ *
12
+ * It targets the IGNIS REST contract - a list route that answers `Content-Range` - and promises
13
+ * nothing about an arbitrary REST API. How a filter serialises and which header carries the total
14
+ * are one API's CONVENTIONS; IGNIS talking to IGNIS is both ends speaking its own.
15
+ */
16
+ export declare class HttpDataSource extends AbstractDataSource<IHttpDataSourceSettings> {
17
+ name: string;
18
+ settings: IHttpDataSourceSettings;
19
+ /** A remote API publishes no schema to this process. An empty object where a caller expects table metadata is worse than nothing. */
20
+ schema: never;
21
+ /** The framework's own HTTP layer, not a bare `fetch`: timeout, abort, secret redaction and scoped logging come with it, and one HTTP path in the framework beats two. */
22
+ protected network: NodeFetchNetworkRequest;
23
+ constructor(opts: IHttpDataSourceSettings & {
24
+ name?: string;
25
+ });
26
+ /** Nothing to open. Holding no connection is what makes this one safe to construct anywhere. */
27
+ configure(): Promise<void>;
28
+ /** No transactions over REST. The root probes capabilities rather than assuming them. */
29
+ getCapabilities(): {
30
+ transactions: boolean;
31
+ };
32
+ /** Explicit token first, resolver only when there is none - a caller that supplied one is never overridden by a lookup. */
33
+ protected resolveAuthToken(): IAuthToken | undefined;
34
+ /**
35
+ * Built through `Headers` rather than a plain object, so a caller's value genuinely OVERRIDES a
36
+ * default. Two spellings of one name in an object both survive the merge and `Headers` then
37
+ * APPENDS them - `x-request-count: "false, true"` - which parses as neither.
38
+ */
39
+ protected buildHeaders(opts: {
40
+ token?: IAuthToken;
41
+ }): Headers;
42
+ buildUrl(opts: {
43
+ paths: Array<string>;
44
+ query?: Record<string, unknown>;
45
+ }): string;
46
+ /**
47
+ * One request, with the total read from `Content-Range` and REPORTED as absent when the header is
48
+ * not there.
49
+ *
50
+ * Goes through {@link HttpDataSource.request}, so auth, the 401 hook and the single retry live in
51
+ * ONE place - a second header-building path is where a guard gets forgotten.
52
+ */
53
+ /**
54
+ * One request, answered RAW.
55
+ *
56
+ * `read` is about rows and cannot express a response that is not JSON - an export endpoint returns
57
+ * bytes and a filename. Rather than invent a blob shape on the repository, whose contract is rows,
58
+ * the transport stays reachable: a caller gets the `Response` and reads `.blob()` and
59
+ * `content-disposition` itself.
60
+ *
61
+ * Auth, the 401 hook and the single retry apply here exactly as they do to `read`.
62
+ */
63
+ request(opts: {
64
+ paths: Array<string>;
65
+ query?: Record<string, unknown>;
66
+ method?: string;
67
+ }): Promise<Response>;
68
+ read<R>(opts: {
69
+ paths: Array<string>;
70
+ query?: Record<string, unknown>;
71
+ /** What the caller asked for. `one` is never unwrapped - see {@link unwrapBody}. */
72
+ shape?: 'list' | 'one';
73
+ }): Promise<IHttpReadResult<R>>;
74
+ }
75
+ //# sourceMappingURL=datasource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datasource.d.ts","sourceRoot":"","sources":["../../../src/http/datasource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,KAAK,EAAE,UAAU,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAiD3F;;;;;;;;;;;GAWG;AACH,qBAAa,cAAe,SAAQ,kBAAkB,CAAC,uBAAuB,CAAC;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,uBAAuB,CAAC;IAC3C,qIAAqI;IAC5H,MAAM,EAAS,KAAK,CAAC;IAE9B,0KAA0K;IAC1K,SAAS,CAAC,OAAO,EAAE,uBAAuB,CAAC;gBAE/B,IAAI,EAAE,uBAAuB,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;IAY7D,gGAAgG;IAC1F,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAEhC,yFAAyF;IAChF,eAAe;;;IAIxB,2HAA2H;IAC3H,SAAS,CAAC,gBAAgB,IAAI,UAAU,GAAG,SAAS;IAIpD;;;;OAIG;IACH,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,UAAU,CAAA;KAAE,GAAG,OAAO;IA4B7D,QAAQ,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,MAAM;IAkBjF;;;;;;OAMG;IACH;;;;;;;;;OASG;IACG,OAAO,CAAC,IAAI,EAAE;QAClB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAuBf,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE;QAClB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,oFAAoF;QACpF,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;KACxB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;CAsBhC"}
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpDataSource = void 0;
4
+ const repository_1 = require("@venizia/ignis-kernel/repository");
5
+ const common_1 = require("@venizia/ignis-helpers/common");
6
+ const core_1 = require("@venizia/ignis-helpers/core");
7
+ const core_2 = require("@venizia/ignis-helpers/core");
8
+ /**
9
+ * Both list shapes IGNIS itself answers with, told apart by structure.
10
+ *
11
+ * `x-request-count` decides it, and its DEFAULT is on (`rest/base.ts`: `?? 'true'`), so an IGNIS
12
+ * list route answers the count envelope unless asked otherwise. This datasource asks for the bare
13
+ * array, but a hand-written route that never reaches `normalizeCountData` can still answer the
14
+ * envelope - so both are accepted rather than one assumed. Treating an envelope as a row is the
15
+ * failure that reports one record where there were fifty, with no error anywhere.
16
+ *
17
+ * The envelope `count` is the rows of THAT response, never the total - the total is only ever in
18
+ * `Content-Range`.
19
+ *
20
+ * `shape` is asked for rather than sniffed. Structure alone cannot tell an envelope from a record
21
+ * that happens to carry a `data` column and a `count` column, and guessing wrong there unwraps a
22
+ * row into whatever its `data` field held.
23
+ */
24
+ const unwrapBody = (opts) => {
25
+ const { body, shape } = opts;
26
+ if (shape === 'one' || Array.isArray(body)) {
27
+ return body;
28
+ }
29
+ if (body && typeof body === 'object' && 'data' in body && 'count' in body) {
30
+ return body.data;
31
+ }
32
+ return body;
33
+ };
34
+ /** `items 0-24/137` -> `{ skip: 0, total: 137 }`. Absent or unparseable answers `undefined`, never a guess. */
35
+ const readContentRange = (opts) => {
36
+ const { header } = opts;
37
+ if (!header) {
38
+ return undefined;
39
+ }
40
+ const match = header.match(/(\d+)\s*-\s*(\d+)\s*\/\s*(\d+)/);
41
+ if (!match) {
42
+ return undefined;
43
+ }
44
+ return { skip: Number(match[1]), total: Number(match[3]) };
45
+ };
46
+ /**
47
+ * A datasource whose transport is HTTP.
48
+ *
49
+ * A repository talks to a datasource, and `AbstractDataSource` is engine-neutral - its only required
50
+ * member is `configure()`. So "a request to another IGNIS server" is a datasource in the same sense
51
+ * Drizzle-over-Postgres is one: same role, different transport, and the same
52
+ * `@venizia/ignis-filter` vocabulary on the way in.
53
+ *
54
+ * It targets the IGNIS REST contract - a list route that answers `Content-Range` - and promises
55
+ * nothing about an arbitrary REST API. How a filter serialises and which header carries the total
56
+ * are one API's CONVENTIONS; IGNIS talking to IGNIS is both ends speaking its own.
57
+ */
58
+ class HttpDataSource extends repository_1.AbstractDataSource {
59
+ constructor(opts) {
60
+ super({ scope: opts.name ?? HttpDataSource.name });
61
+ /** A remote API publishes no schema to this process. An empty object where a caller expects table metadata is worse than nothing. */
62
+ this.schema = {};
63
+ const { name = 'http', ...settings } = opts;
64
+ this.name = name;
65
+ this.settings = settings;
66
+ this.network = new core_1.NodeFetchNetworkRequest({
67
+ name,
68
+ networkOptions: { baseUrl: settings.baseUrl },
69
+ });
70
+ }
71
+ /** Nothing to open. Holding no connection is what makes this one safe to construct anywhere. */
72
+ async configure() { }
73
+ /** No transactions over REST. The root probes capabilities rather than assuming them. */
74
+ getCapabilities() {
75
+ return { transactions: false };
76
+ }
77
+ /** Explicit token first, resolver only when there is none - a caller that supplied one is never overridden by a lookup. */
78
+ resolveAuthToken() {
79
+ return this.settings.authToken ?? this.settings.authTokenResolver?.();
80
+ }
81
+ /**
82
+ * Built through `Headers` rather than a plain object, so a caller's value genuinely OVERRIDES a
83
+ * default. Two spellings of one name in an object both survive the merge and `Headers` then
84
+ * APPENDS them - `x-request-count: "false, true"` - which parses as neither.
85
+ */
86
+ buildHeaders(opts) {
87
+ const { token } = opts;
88
+ const headers = new Headers();
89
+ // Asked explicitly: the server default is the count envelope, and a list that answers rows is
90
+ // one less shape to tell apart. Set FIRST so a caller can override it.
91
+ headers.set(common_1.HTTP.Headers.REQUEST_COUNT_DATA, 'false');
92
+ const configured = [...new Headers(this.settings.headers).entries()];
93
+ for (const [name, value] of configured) {
94
+ headers.set(name, value);
95
+ }
96
+ if (!token) {
97
+ return headers;
98
+ }
99
+ headers.set(common_1.HTTP.Headers.AUTHORIZATION, `${token.type ?? 'Bearer'} ${token.value}`);
100
+ // Only when there IS one. Setting it unconditionally sends the string "undefined", which a
101
+ // server reads as a provider by that name.
102
+ if (token.provider) {
103
+ headers.set('x-auth-provider', token.provider);
104
+ }
105
+ return headers;
106
+ }
107
+ buildUrl(opts) {
108
+ const { paths, query } = opts;
109
+ // Path joining belongs to the network helper - it already normalises leading slashes and
110
+ // refuses an unset base url with a catalogued error.
111
+ const url = new URL(this.network.getRequestUrl({ paths }));
112
+ const parameters = Object.entries(query ?? {});
113
+ for (const [key, value] of parameters) {
114
+ if (value === undefined) {
115
+ continue;
116
+ }
117
+ url.searchParams.set(key, typeof value === 'string' ? value : JSON.stringify(value));
118
+ }
119
+ return url.toString();
120
+ }
121
+ /**
122
+ * One request, with the total read from `Content-Range` and REPORTED as absent when the header is
123
+ * not there.
124
+ *
125
+ * Goes through {@link HttpDataSource.request}, so auth, the 401 hook and the single retry live in
126
+ * ONE place - a second header-building path is where a guard gets forgotten.
127
+ */
128
+ /**
129
+ * One request, answered RAW.
130
+ *
131
+ * `read` is about rows and cannot express a response that is not JSON - an export endpoint returns
132
+ * bytes and a filename. Rather than invent a blob shape on the repository, whose contract is rows,
133
+ * the transport stays reachable: a caller gets the `Response` and reads `.blob()` and
134
+ * `content-disposition` itself.
135
+ *
136
+ * Auth, the 401 hook and the single retry apply here exactly as they do to `read`.
137
+ */
138
+ async request(opts) {
139
+ const url = this.buildUrl(opts);
140
+ const send = () => this.network.getNetworkService().send({
141
+ url,
142
+ method: (opts.method ?? common_1.HTTP.Methods.GET),
143
+ headers: this.buildHeaders({ token: this.resolveAuthToken() }),
144
+ });
145
+ let response = await send();
146
+ if (response.status === common_1.HTTP.ResultCodes.RS_4.Unauthorized && this.settings.onUnauthorized) {
147
+ const shouldRetry = await this.settings.onUnauthorized();
148
+ if (shouldRetry) {
149
+ response = await send();
150
+ }
151
+ }
152
+ return response;
153
+ }
154
+ async read(opts) {
155
+ const response = await this.request(opts);
156
+ if (!response.ok) {
157
+ throw (0, core_2.getError)({
158
+ statusCode: response.status,
159
+ message: `[${this.name}][read] ${response.status} | ${this.buildUrl(opts)}`,
160
+ });
161
+ }
162
+ const body = await response.json();
163
+ const range = readContentRange({ header: response.headers.get(common_1.HTTP.Headers.CONTENT_RANGE) });
164
+ const data = unwrapBody({ body, shape: opts.shape ?? 'list' });
165
+ return {
166
+ data,
167
+ hasRange: range !== undefined,
168
+ total: range?.total,
169
+ skip: range?.skip,
170
+ dataLength: Array.isArray(data) ? data.length : 1,
171
+ };
172
+ }
173
+ }
174
+ exports.HttpDataSource = HttpDataSource;
175
+ //# sourceMappingURL=datasource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datasource.js","sourceRoot":"","sources":["../../../src/http/datasource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from './common/types';
2
+ export * from './datasource';
3
+ export * from './repository';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/http/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./common/types"), exports);
18
+ __exportStar(require("./datasource"), exports);
19
+ __exportStar(require("./repository"), exports);
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iDAA+B;AAC/B,+CAA6B;AAC7B,+CAA6B"}
@@ -0,0 +1,72 @@
1
+ import { AbstractEntity } from '@venizia/ignis-kernel/repository';
2
+ import type { IReadableRepository, TCount, TDataWithRange, TFilter, TSchemaType, TWhere } from '@venizia/ignis-kernel/repository';
3
+ import type { AnyType } from '@venizia/ignis-helpers/common';
4
+ import type { HttpDataSource } from './datasource';
5
+ /** The entity a remote resource stands for. `AbstractEntity` wants a name and a schema accessor; a resource over HTTP has the first and cannot honestly claim the second. */
6
+ export declare class HttpResourceEntity extends AbstractEntity {
7
+ getSchema<T = unknown>(_opts: {
8
+ type: TSchemaType;
9
+ }): T;
10
+ }
11
+ /**
12
+ * Read access to one remote resource, through {@link HttpDataSource}.
13
+ *
14
+ * `IReadableRepository` rather than `AbstractRepository`: the abstract class demands twelve members,
15
+ * so a resource that cannot be written would have to stub `create`, `updateById` and `deleteById`
16
+ * with throws. Five real methods say more than twelve where seven are lies.
17
+ *
18
+ * Every query leaves as the `@venizia/ignis-filter` vocabulary - the same language an IGNIS
19
+ * repository speaks to Drizzle. One query language, two transports.
20
+ */
21
+ export declare class HttpRepository<E extends object = AnyType> implements IReadableRepository<E, {}> {
22
+ dataSource: HttpDataSource;
23
+ entity: AbstractEntity;
24
+ protected resource: string;
25
+ /** An API that publishes a count route names it here. Absent, the total comes from `Content-Range`. */
26
+ protected countPath?: string;
27
+ constructor(opts: {
28
+ dataSource: HttpDataSource;
29
+ resource: string;
30
+ countPath?: string;
31
+ });
32
+ getEntity(): AbstractEntity;
33
+ /**
34
+ * How many rows match, asked of the list route rather than of one that may not exist.
35
+ *
36
+ * The default reads the total out of `Content-Range` - MECHANISM, because a route that pages at
37
+ * all reports what it is paging over. A dedicated count route is CONVENTION: one API publishes
38
+ * `/count`, another `/search/count`, another deleted it and told callers to read the header. So it
39
+ * is opt-in through `countPath`.
40
+ *
41
+ * With no header there is NO total, and this says so rather than answering with the page size - a
42
+ * count of 1 for a table of 7000 reads as healthy in every screen that consumes it.
43
+ */
44
+ count(opts: {
45
+ where: TWhere<E>;
46
+ options?: {};
47
+ }): Promise<TCount>;
48
+ existsWith(opts: {
49
+ where: TWhere<E>;
50
+ options?: {};
51
+ }): Promise<boolean>;
52
+ find<R = E>(opts: {
53
+ filter: TFilter<E>;
54
+ options: {
55
+ shouldQueryRange: true;
56
+ };
57
+ }): Promise<TDataWithRange<R>>;
58
+ find<R = E>(opts: {
59
+ filter?: TFilter<E>;
60
+ options?: {};
61
+ }): Promise<Array<R>>;
62
+ findOne<R = E>(opts: {
63
+ filter?: TFilter<E>;
64
+ options?: {};
65
+ }): Promise<R | null>;
66
+ findById<R = E>(opts: {
67
+ id: string | number;
68
+ filter?: TFilter<E>;
69
+ options?: {};
70
+ }): Promise<R | null>;
71
+ }
72
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../../src/http/repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAkB,MAAM,kCAAkC,CAAC;AAClF,OAAO,KAAK,EACV,mBAAmB,EACnB,MAAM,EACN,cAAc,EACd,OAAO,EACP,WAAW,EACX,MAAM,EACP,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,6KAA6K;AAC7K,qBAAa,kBAAmB,SAAQ,cAAc;IACpD,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,GAAG,CAAC;CAGxD;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,OAAO,CAAE,YAAW,mBAAmB,CAAC,CAAC,EAAE,EAAE,CAAC;IAC3F,UAAU,EAAE,cAAc,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;IAEvB,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC3B,uGAAuG;IACvG,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;gBAEjB,IAAI,EAAE;QAAE,UAAU,EAAE,cAAc,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAOtF,SAAS,IAAI,cAAc;IAI3B;;;;;;;;;;OAUG;IACG,KAAK,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BhE,UAAU,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtE,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QACtB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QACnB,OAAO,EAAE;YAAE,gBAAgB,EAAE,IAAI,CAAA;SAAE,CAAC;KACrC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAgC3E,OAAO,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAK9E,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAC1B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QACpB,OAAO,CAAC,EAAE,EAAE,CAAC;KACd,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAStB"}
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpRepository = exports.HttpResourceEntity = void 0;
4
+ const repository_1 = require("@venizia/ignis-kernel/repository");
5
+ const core_1 = require("@venizia/ignis-helpers/core");
6
+ /** The entity a remote resource stands for. `AbstractEntity` wants a name and a schema accessor; a resource over HTTP has the first and cannot honestly claim the second. */
7
+ class HttpResourceEntity extends repository_1.AbstractEntity {
8
+ getSchema(_opts) {
9
+ return {};
10
+ }
11
+ }
12
+ exports.HttpResourceEntity = HttpResourceEntity;
13
+ /**
14
+ * Read access to one remote resource, through {@link HttpDataSource}.
15
+ *
16
+ * `IReadableRepository` rather than `AbstractRepository`: the abstract class demands twelve members,
17
+ * so a resource that cannot be written would have to stub `create`, `updateById` and `deleteById`
18
+ * with throws. Five real methods say more than twelve where seven are lies.
19
+ *
20
+ * Every query leaves as the `@venizia/ignis-filter` vocabulary - the same language an IGNIS
21
+ * repository speaks to Drizzle. One query language, two transports.
22
+ */
23
+ class HttpRepository {
24
+ constructor(opts) {
25
+ this.dataSource = opts.dataSource;
26
+ this.resource = opts.resource;
27
+ this.countPath = opts.countPath;
28
+ this.entity = new HttpResourceEntity({ name: opts.resource });
29
+ }
30
+ getEntity() {
31
+ return this.entity;
32
+ }
33
+ /**
34
+ * How many rows match, asked of the list route rather than of one that may not exist.
35
+ *
36
+ * The default reads the total out of `Content-Range` - MECHANISM, because a route that pages at
37
+ * all reports what it is paging over. A dedicated count route is CONVENTION: one API publishes
38
+ * `/count`, another `/search/count`, another deleted it and told callers to read the header. So it
39
+ * is opt-in through `countPath`.
40
+ *
41
+ * With no header there is NO total, and this says so rather than answering with the page size - a
42
+ * count of 1 for a table of 7000 reads as healthy in every screen that consumes it.
43
+ */
44
+ async count(opts) {
45
+ if (this.countPath) {
46
+ const rs = await this.dataSource.read({
47
+ paths: [this.resource, this.countPath],
48
+ query: { where: opts.where },
49
+ shape: 'one',
50
+ });
51
+ return { count: rs.data?.count ?? 0 };
52
+ }
53
+ const rs = await this.dataSource.read({
54
+ paths: [this.resource],
55
+ query: { filter: { where: opts.where, limit: 1 } },
56
+ });
57
+ if (!rs.hasRange) {
58
+ throw (0, core_1.getError)({
59
+ statusCode: 500,
60
+ message: `[${this.resource}][count] The response carried no Content-Range, so there is no total to report - answering with the page size would claim ${rs.dataLength}. Give this repository a countPath if the API publishes a count route.`,
61
+ });
62
+ }
63
+ return { count: rs.total ?? 0 };
64
+ }
65
+ async existsWith(opts) {
66
+ const rs = await this.dataSource.read({
67
+ paths: [this.resource],
68
+ query: { filter: { where: opts.where, limit: 1 } },
69
+ });
70
+ // Asked of the rows, not of the total: one row proves existence with no header needed.
71
+ return rs.dataLength > 0;
72
+ }
73
+ async find(opts) {
74
+ const rs = await this.dataSource.read({
75
+ paths: [this.resource],
76
+ query: { filter: opts.filter ?? {} },
77
+ });
78
+ const data = rs.data ?? [];
79
+ if (!opts.options?.shouldQueryRange) {
80
+ return data;
81
+ }
82
+ if (!rs.hasRange) {
83
+ throw (0, core_1.getError)({
84
+ statusCode: 500,
85
+ message: `[${this.resource}][find] A range was asked for and the response carried no Content-Range. Deriving it from the page would report ${data.length} as the total.`,
86
+ });
87
+ }
88
+ // `TDataRange` follows the Content-Range standard, and `content-range` is the header this
89
+ // transport receives - the envelope IGNIS paginates with and the protocol are the same thing,
90
+ // so nothing is translated here.
91
+ return {
92
+ data,
93
+ range: (0, repository_1.buildDataRange)({ skip: rs.skip, dataLength: data.length, total: rs.total ?? 0 }),
94
+ };
95
+ }
96
+ async findOne(opts) {
97
+ const rs = await this.find({ filter: { ...(opts.filter ?? {}), limit: 1 } });
98
+ return rs[0] ?? null;
99
+ }
100
+ async findById(opts) {
101
+ const rs = await this.dataSource.read({
102
+ paths: [this.resource, `${opts.id}`],
103
+ query: { filter: opts.filter ?? {} },
104
+ shape: 'one',
105
+ });
106
+ return rs.data ?? null;
107
+ }
108
+ }
109
+ exports.HttpRepository = HttpRepository;
110
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../../../src/http/repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { TFilter } from '@venizia/ignis-filter';
2
+ /** What a caller hands the resolver seam: the token and, optionally, which provider issued it. */
3
+ export interface IAuthToken {
4
+ value: string;
5
+ type?: string;
6
+ provider?: string;
7
+ }
8
+ /**
9
+ * Where the token comes from when the datasource was not given one outright.
10
+ *
11
+ * A seam, not a guard: a browser resolver reads storage, a server resolver reads a secret, and
12
+ * neither is baked into the transport. Returning `undefined` means "no token", which is different
13
+ * from an empty one.
14
+ */
15
+ export type TAuthTokenResolver = () => IAuthToken | undefined;
16
+ /**
17
+ * Every shape `Headers` accepts. Not `Record<string, string>`: header names are case-INSENSITIVE,
18
+ * so a plain object lets two spellings of one name coexist, and merging them APPENDS rather than
19
+ * overrides - `{ 'x-request-count': 'false', 'X-Request-Count': 'true' }` reaches the wire as
20
+ * `x-request-count: "false, true"`, which parses as neither.
21
+ */
22
+ export type THttpHeaders = Headers | Array<[string, string]> | Record<string, string>;
23
+ export interface IHttpDataSourceSettings {
24
+ baseUrl: string;
25
+ /** Sent on every request, merged through `Headers` so a caller value overrides a default. */
26
+ headers?: THttpHeaders;
27
+ /** A fixed token. Consulted BEFORE `authTokenResolver` - an explicit value is never overridden by a lookup. */
28
+ authToken?: IAuthToken;
29
+ authTokenResolver?: TAuthTokenResolver;
30
+ /**
31
+ * Runs on a 401, and its ANSWER decides: `true` retries the request once, `false` lets the 401
32
+ * stand. Absent means no retry at all.
33
+ *
34
+ * A hook rather than a boolean, because "recover from a 401" is the host's policy, not the
35
+ * transport's: one host refreshes a token then retries, another logs the user out, a third does
36
+ * both in an order only it knows. A boolean could only mean "ask the resolver again", which reads
37
+ * like recovery and is not - someone would wire it expecting a refresh and get a silent loop.
38
+ *
39
+ * The transport's part is the seam and the single retry. Whatever the hook does to make the next
40
+ * attempt work - refresh, re-login, nothing - happens before it answers, and the retry re-resolves
41
+ * the token afterwards.
42
+ */
43
+ onUnauthorized?: () => Promise<boolean> | boolean;
44
+ }
45
+ /**
46
+ * One response, with the total kept apart from the rows.
47
+ *
48
+ * `hasRange` is the whole point: a list with no `Content-Range` has NO total, which is a different
49
+ * answer from "the total equals this page". Folding the two is how a count of 7000 reports 1 and
50
+ * every screen reading it looks healthy.
51
+ */
52
+ export interface IHttpReadResult<R> {
53
+ data: R;
54
+ hasRange: boolean;
55
+ total?: number;
56
+ skip?: number;
57
+ dataLength: number;
58
+ }
59
+ export type THttpQuery<E extends object> = {
60
+ filter?: TFilter<E>;
61
+ };
62
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/http/common/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,kGAAkG;AAClG,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,UAAU,GAAG,SAAS,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACtF,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAEhB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,+GAA+G;IAC/G,SAAS,CAAC,EAAE,UAAU,CAAC;IAEvB,iBAAiB,CAAC,EAAE,kBAAkB,CAAC;IAEvC;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACnD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/http/common/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import { AbstractDataSource } from '@venizia/ignis-kernel/repository';
2
+ import { NodeFetchNetworkRequest } from '@venizia/ignis-helpers/core';
3
+ import type { IAuthToken, IHttpDataSourceSettings, IHttpReadResult } from './common/types.js';
4
+ /**
5
+ * A datasource whose transport is HTTP.
6
+ *
7
+ * A repository talks to a datasource, and `AbstractDataSource` is engine-neutral - its only required
8
+ * member is `configure()`. So "a request to another IGNIS server" is a datasource in the same sense
9
+ * Drizzle-over-Postgres is one: same role, different transport, and the same
10
+ * `@venizia/ignis-filter` vocabulary on the way in.
11
+ *
12
+ * It targets the IGNIS REST contract - a list route that answers `Content-Range` - and promises
13
+ * nothing about an arbitrary REST API. How a filter serialises and which header carries the total
14
+ * are one API's CONVENTIONS; IGNIS talking to IGNIS is both ends speaking its own.
15
+ */
16
+ export declare class HttpDataSource extends AbstractDataSource<IHttpDataSourceSettings> {
17
+ name: string;
18
+ settings: IHttpDataSourceSettings;
19
+ /** A remote API publishes no schema to this process. An empty object where a caller expects table metadata is worse than nothing. */
20
+ schema: never;
21
+ /** The framework's own HTTP layer, not a bare `fetch`: timeout, abort, secret redaction and scoped logging come with it, and one HTTP path in the framework beats two. */
22
+ protected network: NodeFetchNetworkRequest;
23
+ constructor(opts: IHttpDataSourceSettings & {
24
+ name?: string;
25
+ });
26
+ /** Nothing to open. Holding no connection is what makes this one safe to construct anywhere. */
27
+ configure(): Promise<void>;
28
+ /** No transactions over REST. The root probes capabilities rather than assuming them. */
29
+ getCapabilities(): {
30
+ transactions: boolean;
31
+ };
32
+ /** Explicit token first, resolver only when there is none - a caller that supplied one is never overridden by a lookup. */
33
+ protected resolveAuthToken(): IAuthToken | undefined;
34
+ /**
35
+ * Built through `Headers` rather than a plain object, so a caller's value genuinely OVERRIDES a
36
+ * default. Two spellings of one name in an object both survive the merge and `Headers` then
37
+ * APPENDS them - `x-request-count: "false, true"` - which parses as neither.
38
+ */
39
+ protected buildHeaders(opts: {
40
+ token?: IAuthToken;
41
+ }): Headers;
42
+ buildUrl(opts: {
43
+ paths: Array<string>;
44
+ query?: Record<string, unknown>;
45
+ }): string;
46
+ /**
47
+ * One request, with the total read from `Content-Range` and REPORTED as absent when the header is
48
+ * not there.
49
+ *
50
+ * Goes through {@link HttpDataSource.request}, so auth, the 401 hook and the single retry live in
51
+ * ONE place - a second header-building path is where a guard gets forgotten.
52
+ */
53
+ /**
54
+ * One request, answered RAW.
55
+ *
56
+ * `read` is about rows and cannot express a response that is not JSON - an export endpoint returns
57
+ * bytes and a filename. Rather than invent a blob shape on the repository, whose contract is rows,
58
+ * the transport stays reachable: a caller gets the `Response` and reads `.blob()` and
59
+ * `content-disposition` itself.
60
+ *
61
+ * Auth, the 401 hook and the single retry apply here exactly as they do to `read`.
62
+ */
63
+ request(opts: {
64
+ paths: Array<string>;
65
+ query?: Record<string, unknown>;
66
+ method?: string;
67
+ }): Promise<Response>;
68
+ read<R>(opts: {
69
+ paths: Array<string>;
70
+ query?: Record<string, unknown>;
71
+ /** What the caller asked for. `one` is never unwrapped - see {@link unwrapBody}. */
72
+ shape?: 'list' | 'one';
73
+ }): Promise<IHttpReadResult<R>>;
74
+ }
75
+ //# sourceMappingURL=datasource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datasource.d.ts","sourceRoot":"","sources":["../../../src/http/datasource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,KAAK,EAAE,UAAU,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAiD3F;;;;;;;;;;;GAWG;AACH,qBAAa,cAAe,SAAQ,kBAAkB,CAAC,uBAAuB,CAAC;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,uBAAuB,CAAC;IAC3C,qIAAqI;IAC5H,MAAM,EAAS,KAAK,CAAC;IAE9B,0KAA0K;IAC1K,SAAS,CAAC,OAAO,EAAE,uBAAuB,CAAC;gBAE/B,IAAI,EAAE,uBAAuB,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;IAY7D,gGAAgG;IAC1F,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAEhC,yFAAyF;IAChF,eAAe;;;IAIxB,2HAA2H;IAC3H,SAAS,CAAC,gBAAgB,IAAI,UAAU,GAAG,SAAS;IAIpD;;;;OAIG;IACH,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,UAAU,CAAA;KAAE,GAAG,OAAO;IA4B7D,QAAQ,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,MAAM;IAkBjF;;;;;;OAMG;IACH;;;;;;;;;OASG;IACG,OAAO,CAAC,IAAI,EAAE;QAClB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAuBf,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE;QAClB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,oFAAoF;QACpF,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;KACxB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;CAsBhC"}
@@ -0,0 +1,171 @@
1
+ import { AbstractDataSource } from '@venizia/ignis-kernel/repository';
2
+ import { HTTP } from '@venizia/ignis-helpers/common';
3
+ import { NodeFetchNetworkRequest } from '@venizia/ignis-helpers/core';
4
+ import { getError } from '@venizia/ignis-helpers/core';
5
+ /**
6
+ * Both list shapes IGNIS itself answers with, told apart by structure.
7
+ *
8
+ * `x-request-count` decides it, and its DEFAULT is on (`rest/base.ts`: `?? 'true'`), so an IGNIS
9
+ * list route answers the count envelope unless asked otherwise. This datasource asks for the bare
10
+ * array, but a hand-written route that never reaches `normalizeCountData` can still answer the
11
+ * envelope - so both are accepted rather than one assumed. Treating an envelope as a row is the
12
+ * failure that reports one record where there were fifty, with no error anywhere.
13
+ *
14
+ * The envelope `count` is the rows of THAT response, never the total - the total is only ever in
15
+ * `Content-Range`.
16
+ *
17
+ * `shape` is asked for rather than sniffed. Structure alone cannot tell an envelope from a record
18
+ * that happens to carry a `data` column and a `count` column, and guessing wrong there unwraps a
19
+ * row into whatever its `data` field held.
20
+ */
21
+ const unwrapBody = (opts) => {
22
+ const { body, shape } = opts;
23
+ if (shape === 'one' || Array.isArray(body)) {
24
+ return body;
25
+ }
26
+ if (body && typeof body === 'object' && 'data' in body && 'count' in body) {
27
+ return body.data;
28
+ }
29
+ return body;
30
+ };
31
+ /** `items 0-24/137` -> `{ skip: 0, total: 137 }`. Absent or unparseable answers `undefined`, never a guess. */
32
+ const readContentRange = (opts) => {
33
+ const { header } = opts;
34
+ if (!header) {
35
+ return undefined;
36
+ }
37
+ const match = header.match(/(\d+)\s*-\s*(\d+)\s*\/\s*(\d+)/);
38
+ if (!match) {
39
+ return undefined;
40
+ }
41
+ return { skip: Number(match[1]), total: Number(match[3]) };
42
+ };
43
+ /**
44
+ * A datasource whose transport is HTTP.
45
+ *
46
+ * A repository talks to a datasource, and `AbstractDataSource` is engine-neutral - its only required
47
+ * member is `configure()`. So "a request to another IGNIS server" is a datasource in the same sense
48
+ * Drizzle-over-Postgres is one: same role, different transport, and the same
49
+ * `@venizia/ignis-filter` vocabulary on the way in.
50
+ *
51
+ * It targets the IGNIS REST contract - a list route that answers `Content-Range` - and promises
52
+ * nothing about an arbitrary REST API. How a filter serialises and which header carries the total
53
+ * are one API's CONVENTIONS; IGNIS talking to IGNIS is both ends speaking its own.
54
+ */
55
+ export class HttpDataSource extends AbstractDataSource {
56
+ constructor(opts) {
57
+ super({ scope: opts.name ?? HttpDataSource.name });
58
+ /** A remote API publishes no schema to this process. An empty object where a caller expects table metadata is worse than nothing. */
59
+ this.schema = {};
60
+ const { name = 'http', ...settings } = opts;
61
+ this.name = name;
62
+ this.settings = settings;
63
+ this.network = new NodeFetchNetworkRequest({
64
+ name,
65
+ networkOptions: { baseUrl: settings.baseUrl },
66
+ });
67
+ }
68
+ /** Nothing to open. Holding no connection is what makes this one safe to construct anywhere. */
69
+ async configure() { }
70
+ /** No transactions over REST. The root probes capabilities rather than assuming them. */
71
+ getCapabilities() {
72
+ return { transactions: false };
73
+ }
74
+ /** Explicit token first, resolver only when there is none - a caller that supplied one is never overridden by a lookup. */
75
+ resolveAuthToken() {
76
+ return this.settings.authToken ?? this.settings.authTokenResolver?.();
77
+ }
78
+ /**
79
+ * Built through `Headers` rather than a plain object, so a caller's value genuinely OVERRIDES a
80
+ * default. Two spellings of one name in an object both survive the merge and `Headers` then
81
+ * APPENDS them - `x-request-count: "false, true"` - which parses as neither.
82
+ */
83
+ buildHeaders(opts) {
84
+ const { token } = opts;
85
+ const headers = new Headers();
86
+ // Asked explicitly: the server default is the count envelope, and a list that answers rows is
87
+ // one less shape to tell apart. Set FIRST so a caller can override it.
88
+ headers.set(HTTP.Headers.REQUEST_COUNT_DATA, 'false');
89
+ const configured = [...new Headers(this.settings.headers).entries()];
90
+ for (const [name, value] of configured) {
91
+ headers.set(name, value);
92
+ }
93
+ if (!token) {
94
+ return headers;
95
+ }
96
+ headers.set(HTTP.Headers.AUTHORIZATION, `${token.type ?? 'Bearer'} ${token.value}`);
97
+ // Only when there IS one. Setting it unconditionally sends the string "undefined", which a
98
+ // server reads as a provider by that name.
99
+ if (token.provider) {
100
+ headers.set('x-auth-provider', token.provider);
101
+ }
102
+ return headers;
103
+ }
104
+ buildUrl(opts) {
105
+ const { paths, query } = opts;
106
+ // Path joining belongs to the network helper - it already normalises leading slashes and
107
+ // refuses an unset base url with a catalogued error.
108
+ const url = new URL(this.network.getRequestUrl({ paths }));
109
+ const parameters = Object.entries(query ?? {});
110
+ for (const [key, value] of parameters) {
111
+ if (value === undefined) {
112
+ continue;
113
+ }
114
+ url.searchParams.set(key, typeof value === 'string' ? value : JSON.stringify(value));
115
+ }
116
+ return url.toString();
117
+ }
118
+ /**
119
+ * One request, with the total read from `Content-Range` and REPORTED as absent when the header is
120
+ * not there.
121
+ *
122
+ * Goes through {@link HttpDataSource.request}, so auth, the 401 hook and the single retry live in
123
+ * ONE place - a second header-building path is where a guard gets forgotten.
124
+ */
125
+ /**
126
+ * One request, answered RAW.
127
+ *
128
+ * `read` is about rows and cannot express a response that is not JSON - an export endpoint returns
129
+ * bytes and a filename. Rather than invent a blob shape on the repository, whose contract is rows,
130
+ * the transport stays reachable: a caller gets the `Response` and reads `.blob()` and
131
+ * `content-disposition` itself.
132
+ *
133
+ * Auth, the 401 hook and the single retry apply here exactly as they do to `read`.
134
+ */
135
+ async request(opts) {
136
+ const url = this.buildUrl(opts);
137
+ const send = () => this.network.getNetworkService().send({
138
+ url,
139
+ method: (opts.method ?? HTTP.Methods.GET),
140
+ headers: this.buildHeaders({ token: this.resolveAuthToken() }),
141
+ });
142
+ let response = await send();
143
+ if (response.status === HTTP.ResultCodes.RS_4.Unauthorized && this.settings.onUnauthorized) {
144
+ const shouldRetry = await this.settings.onUnauthorized();
145
+ if (shouldRetry) {
146
+ response = await send();
147
+ }
148
+ }
149
+ return response;
150
+ }
151
+ async read(opts) {
152
+ const response = await this.request(opts);
153
+ if (!response.ok) {
154
+ throw getError({
155
+ statusCode: response.status,
156
+ message: `[${this.name}][read] ${response.status} | ${this.buildUrl(opts)}`,
157
+ });
158
+ }
159
+ const body = await response.json();
160
+ const range = readContentRange({ header: response.headers.get(HTTP.Headers.CONTENT_RANGE) });
161
+ const data = unwrapBody({ body, shape: opts.shape ?? 'list' });
162
+ return {
163
+ data,
164
+ hasRange: range !== undefined,
165
+ total: range?.total,
166
+ skip: range?.skip,
167
+ dataLength: Array.isArray(data) ? data.length : 1,
168
+ };
169
+ }
170
+ }
171
+ //# sourceMappingURL=datasource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datasource.js","sourceRoot":"","sources":["../../../src/http/datasource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from './common/types.js';
2
+ export * from './datasource.js';
3
+ export * from './repository.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/http/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './common/types.js';
2
+ export * from './datasource.js';
3
+ export * from './repository.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { AbstractEntity } from '@venizia/ignis-kernel/repository';
2
+ import type { IReadableRepository, TCount, TDataWithRange, TFilter, TSchemaType, TWhere } from '@venizia/ignis-kernel/repository';
3
+ import type { AnyType } from '@venizia/ignis-helpers/common';
4
+ import type { HttpDataSource } from './datasource.js';
5
+ /** The entity a remote resource stands for. `AbstractEntity` wants a name and a schema accessor; a resource over HTTP has the first and cannot honestly claim the second. */
6
+ export declare class HttpResourceEntity extends AbstractEntity {
7
+ getSchema<T = unknown>(_opts: {
8
+ type: TSchemaType;
9
+ }): T;
10
+ }
11
+ /**
12
+ * Read access to one remote resource, through {@link HttpDataSource}.
13
+ *
14
+ * `IReadableRepository` rather than `AbstractRepository`: the abstract class demands twelve members,
15
+ * so a resource that cannot be written would have to stub `create`, `updateById` and `deleteById`
16
+ * with throws. Five real methods say more than twelve where seven are lies.
17
+ *
18
+ * Every query leaves as the `@venizia/ignis-filter` vocabulary - the same language an IGNIS
19
+ * repository speaks to Drizzle. One query language, two transports.
20
+ */
21
+ export declare class HttpRepository<E extends object = AnyType> implements IReadableRepository<E, {}> {
22
+ dataSource: HttpDataSource;
23
+ entity: AbstractEntity;
24
+ protected resource: string;
25
+ /** An API that publishes a count route names it here. Absent, the total comes from `Content-Range`. */
26
+ protected countPath?: string;
27
+ constructor(opts: {
28
+ dataSource: HttpDataSource;
29
+ resource: string;
30
+ countPath?: string;
31
+ });
32
+ getEntity(): AbstractEntity;
33
+ /**
34
+ * How many rows match, asked of the list route rather than of one that may not exist.
35
+ *
36
+ * The default reads the total out of `Content-Range` - MECHANISM, because a route that pages at
37
+ * all reports what it is paging over. A dedicated count route is CONVENTION: one API publishes
38
+ * `/count`, another `/search/count`, another deleted it and told callers to read the header. So it
39
+ * is opt-in through `countPath`.
40
+ *
41
+ * With no header there is NO total, and this says so rather than answering with the page size - a
42
+ * count of 1 for a table of 7000 reads as healthy in every screen that consumes it.
43
+ */
44
+ count(opts: {
45
+ where: TWhere<E>;
46
+ options?: {};
47
+ }): Promise<TCount>;
48
+ existsWith(opts: {
49
+ where: TWhere<E>;
50
+ options?: {};
51
+ }): Promise<boolean>;
52
+ find<R = E>(opts: {
53
+ filter: TFilter<E>;
54
+ options: {
55
+ shouldQueryRange: true;
56
+ };
57
+ }): Promise<TDataWithRange<R>>;
58
+ find<R = E>(opts: {
59
+ filter?: TFilter<E>;
60
+ options?: {};
61
+ }): Promise<Array<R>>;
62
+ findOne<R = E>(opts: {
63
+ filter?: TFilter<E>;
64
+ options?: {};
65
+ }): Promise<R | null>;
66
+ findById<R = E>(opts: {
67
+ id: string | number;
68
+ filter?: TFilter<E>;
69
+ options?: {};
70
+ }): Promise<R | null>;
71
+ }
72
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../../src/http/repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAkB,MAAM,kCAAkC,CAAC;AAClF,OAAO,KAAK,EACV,mBAAmB,EACnB,MAAM,EACN,cAAc,EACd,OAAO,EACP,WAAW,EACX,MAAM,EACP,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,6KAA6K;AAC7K,qBAAa,kBAAmB,SAAQ,cAAc;IACpD,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,GAAG,CAAC;CAGxD;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,OAAO,CAAE,YAAW,mBAAmB,CAAC,CAAC,EAAE,EAAE,CAAC;IAC3F,UAAU,EAAE,cAAc,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;IAEvB,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC3B,uGAAuG;IACvG,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;gBAEjB,IAAI,EAAE;QAAE,UAAU,EAAE,cAAc,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAOtF,SAAS,IAAI,cAAc;IAI3B;;;;;;;;;;OAUG;IACG,KAAK,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BhE,UAAU,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtE,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QACtB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QACnB,OAAO,EAAE;YAAE,gBAAgB,EAAE,IAAI,CAAA;SAAE,CAAC;KACrC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAgC3E,OAAO,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAK9E,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAC1B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QACpB,OAAO,CAAC,EAAE,EAAE,CAAC;KACd,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAStB"}
@@ -0,0 +1,105 @@
1
+ import { AbstractEntity, buildDataRange } from '@venizia/ignis-kernel/repository';
2
+ import { getError } from '@venizia/ignis-helpers/core';
3
+ /** The entity a remote resource stands for. `AbstractEntity` wants a name and a schema accessor; a resource over HTTP has the first and cannot honestly claim the second. */
4
+ export class HttpResourceEntity extends AbstractEntity {
5
+ getSchema(_opts) {
6
+ return {};
7
+ }
8
+ }
9
+ /**
10
+ * Read access to one remote resource, through {@link HttpDataSource}.
11
+ *
12
+ * `IReadableRepository` rather than `AbstractRepository`: the abstract class demands twelve members,
13
+ * so a resource that cannot be written would have to stub `create`, `updateById` and `deleteById`
14
+ * with throws. Five real methods say more than twelve where seven are lies.
15
+ *
16
+ * Every query leaves as the `@venizia/ignis-filter` vocabulary - the same language an IGNIS
17
+ * repository speaks to Drizzle. One query language, two transports.
18
+ */
19
+ export class HttpRepository {
20
+ constructor(opts) {
21
+ this.dataSource = opts.dataSource;
22
+ this.resource = opts.resource;
23
+ this.countPath = opts.countPath;
24
+ this.entity = new HttpResourceEntity({ name: opts.resource });
25
+ }
26
+ getEntity() {
27
+ return this.entity;
28
+ }
29
+ /**
30
+ * How many rows match, asked of the list route rather than of one that may not exist.
31
+ *
32
+ * The default reads the total out of `Content-Range` - MECHANISM, because a route that pages at
33
+ * all reports what it is paging over. A dedicated count route is CONVENTION: one API publishes
34
+ * `/count`, another `/search/count`, another deleted it and told callers to read the header. So it
35
+ * is opt-in through `countPath`.
36
+ *
37
+ * With no header there is NO total, and this says so rather than answering with the page size - a
38
+ * count of 1 for a table of 7000 reads as healthy in every screen that consumes it.
39
+ */
40
+ async count(opts) {
41
+ if (this.countPath) {
42
+ const rs = await this.dataSource.read({
43
+ paths: [this.resource, this.countPath],
44
+ query: { where: opts.where },
45
+ shape: 'one',
46
+ });
47
+ return { count: rs.data?.count ?? 0 };
48
+ }
49
+ const rs = await this.dataSource.read({
50
+ paths: [this.resource],
51
+ query: { filter: { where: opts.where, limit: 1 } },
52
+ });
53
+ if (!rs.hasRange) {
54
+ throw getError({
55
+ statusCode: 500,
56
+ message: `[${this.resource}][count] The response carried no Content-Range, so there is no total to report - answering with the page size would claim ${rs.dataLength}. Give this repository a countPath if the API publishes a count route.`,
57
+ });
58
+ }
59
+ return { count: rs.total ?? 0 };
60
+ }
61
+ async existsWith(opts) {
62
+ const rs = await this.dataSource.read({
63
+ paths: [this.resource],
64
+ query: { filter: { where: opts.where, limit: 1 } },
65
+ });
66
+ // Asked of the rows, not of the total: one row proves existence with no header needed.
67
+ return rs.dataLength > 0;
68
+ }
69
+ async find(opts) {
70
+ const rs = await this.dataSource.read({
71
+ paths: [this.resource],
72
+ query: { filter: opts.filter ?? {} },
73
+ });
74
+ const data = rs.data ?? [];
75
+ if (!opts.options?.shouldQueryRange) {
76
+ return data;
77
+ }
78
+ if (!rs.hasRange) {
79
+ throw getError({
80
+ statusCode: 500,
81
+ message: `[${this.resource}][find] A range was asked for and the response carried no Content-Range. Deriving it from the page would report ${data.length} as the total.`,
82
+ });
83
+ }
84
+ // `TDataRange` follows the Content-Range standard, and `content-range` is the header this
85
+ // transport receives - the envelope IGNIS paginates with and the protocol are the same thing,
86
+ // so nothing is translated here.
87
+ return {
88
+ data,
89
+ range: buildDataRange({ skip: rs.skip, dataLength: data.length, total: rs.total ?? 0 }),
90
+ };
91
+ }
92
+ async findOne(opts) {
93
+ const rs = await this.find({ filter: { ...(opts.filter ?? {}), limit: 1 } });
94
+ return rs[0] ?? null;
95
+ }
96
+ async findById(opts) {
97
+ const rs = await this.dataSource.read({
98
+ paths: [this.resource, `${opts.id}`],
99
+ query: { filter: opts.filter ?? {} },
100
+ shape: 'one',
101
+ });
102
+ return rs.data ?? null;
103
+ }
104
+ }
105
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../../../src/http/repository.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venizia/ignis-connectors",
3
- "version": "0.2.0-39",
3
+ "version": "0.2.0-40",
4
4
  "description": "Datasource and repository connectors for the IGNIS framework: the engine-neutral relational and search tiers, plus Postgres, SQLite, PGlite, Typesense and Meilisearch behind sub-paths.",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -16,6 +16,12 @@
16
16
  "require": "./dist/cjs/index.js",
17
17
  "default": "./dist/cjs/index.js"
18
18
  },
19
+ "./http": {
20
+ "types": "./dist/cjs/http/index.d.ts",
21
+ "import": "./dist/esm/http/index.js",
22
+ "require": "./dist/cjs/http/index.js",
23
+ "default": "./dist/cjs/http/index.js"
24
+ },
19
25
  "./relational": {
20
26
  "types": "./dist/cjs/relational/core/index.d.ts",
21
27
  "import": "./dist/esm/relational/core/index.js",
@@ -120,10 +126,10 @@
120
126
  "prepublishOnly": "bun run rebuild"
121
127
  },
122
128
  "dependencies": {
123
- "@venizia/ignis-filter": "^0.2.0-19",
124
- "@venizia/ignis-helpers": "^0.2.0-35",
125
- "@venizia/ignis-inversion": "^0.2.0-19",
126
- "@venizia/ignis-kernel": "^0.2.0-40",
129
+ "@venizia/ignis-filter": "^0.2.0-20",
130
+ "@venizia/ignis-helpers": "^0.2.0-36",
131
+ "@venizia/ignis-inversion": "^0.2.0-20",
132
+ "@venizia/ignis-kernel": "^0.2.0-41",
127
133
  "reflect-metadata": "^0.2.2",
128
134
  "zod": "^4.5.4"
129
135
  },
@@ -162,7 +168,7 @@
162
168
  "devDependencies": {
163
169
  "@types/bun": "^1.4.1",
164
170
  "@types/pg": "^8.23.1",
165
- "@venizia/dev-configs": "^0.2.0-12",
171
+ "@venizia/dev-configs": "^0.2.0-13",
166
172
  "eslint": "^10.10.0",
167
173
  "prettier": "^3.9.6",
168
174
  "tsc-alias": "^1.9.4",