@xata.io/client 0.1.3 → 0.2.2
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/LICENSE +201 -0
- package/dist/index.d.ts +30 -15
- package/dist/index.js +61 -20
- package/dist/index.test.js +149 -27
- package/package.json +3 -2
- package/src/index.test.ts +198 -28
- package/src/index.ts +68 -25
package/LICENSE
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
Apache License
|
2
|
+
Version 2.0, January 2004
|
3
|
+
http://www.apache.org/licenses/
|
4
|
+
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
6
|
+
|
7
|
+
1. Definitions.
|
8
|
+
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
11
|
+
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
13
|
+
the copyright owner that is granting the License.
|
14
|
+
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
16
|
+
other entities that control, are controlled by, or are under common
|
17
|
+
control with that entity. For the purposes of this definition,
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
19
|
+
direction or management of such entity, whether by contract or
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
22
|
+
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
24
|
+
exercising permissions granted by this License.
|
25
|
+
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
27
|
+
including but not limited to software source code, documentation
|
28
|
+
source, and configuration files.
|
29
|
+
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
31
|
+
transformation or translation of a Source form, including but
|
32
|
+
not limited to compiled object code, generated documentation,
|
33
|
+
and conversions to other media types.
|
34
|
+
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
36
|
+
Object form, made available under the License, as indicated by a
|
37
|
+
copyright notice that is included in or attached to the work
|
38
|
+
(an example is provided in the Appendix below).
|
39
|
+
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
46
|
+
the Work and Derivative Works thereof.
|
47
|
+
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
49
|
+
the original version of the Work and any modifications or additions
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
61
|
+
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
64
|
+
subsequently incorporated within the Work.
|
65
|
+
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
72
|
+
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
78
|
+
where such license applies only to those patent claims licensable
|
79
|
+
by such Contributor that are necessarily infringed by their
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
82
|
+
institute patent litigation against any entity (including a
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
85
|
+
or contributory patent infringement, then any patent licenses
|
86
|
+
granted to You under this License for that Work shall terminate
|
87
|
+
as of the date such litigation is filed.
|
88
|
+
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
91
|
+
modifications, and in Source or Object form, provided that You
|
92
|
+
meet the following conditions:
|
93
|
+
|
94
|
+
(a) You must give any other recipients of the Work or
|
95
|
+
Derivative Works a copy of this License; and
|
96
|
+
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
98
|
+
stating that You changed the files; and
|
99
|
+
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
102
|
+
attribution notices from the Source form of the Work,
|
103
|
+
excluding those notices that do not pertain to any part of
|
104
|
+
the Derivative Works; and
|
105
|
+
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
108
|
+
include a readable copy of the attribution notices contained
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
111
|
+
of the following places: within a NOTICE text file distributed
|
112
|
+
as part of the Derivative Works; within the Source form or
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
114
|
+
within a display generated by the Derivative Works, if and
|
115
|
+
wherever such third-party notices normally appear. The contents
|
116
|
+
of the NOTICE file are for informational purposes only and
|
117
|
+
do not modify the License. You may add Your own attribution
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
120
|
+
that such additional attribution notices cannot be construed
|
121
|
+
as modifying the License.
|
122
|
+
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
124
|
+
may provide additional or different license terms and conditions
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
128
|
+
the conditions stated in this License.
|
129
|
+
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
133
|
+
this License, without any additional terms or conditions.
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
135
|
+
the terms of any separate license agreement you may have executed
|
136
|
+
with Licensor regarding such Contributions.
|
137
|
+
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
140
|
+
except as required for reasonable and customary use in describing the
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
142
|
+
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
152
|
+
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
158
|
+
incidental, or consequential damages of any character arising as a
|
159
|
+
result of this License or out of the use or inability to use the
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
162
|
+
other commercial damages or losses), even if such Contributor
|
163
|
+
has been advised of the possibility of such damages.
|
164
|
+
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
168
|
+
or other liability obligations and/or rights consistent with this
|
169
|
+
License. However, in accepting such obligations, You may act only
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
174
|
+
of your accepting any such warranty or additional liability.
|
175
|
+
|
176
|
+
END OF TERMS AND CONDITIONS
|
177
|
+
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
179
|
+
|
180
|
+
To apply the Apache License to your work, attach the following
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
182
|
+
replaced with your own identifying information. (Don't include
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
184
|
+
comment syntax for the file format. We also recommend that a
|
185
|
+
file or class name and description of purpose be included on the
|
186
|
+
same "printed page" as the copyright notice for easier
|
187
|
+
identification within third-party archives.
|
188
|
+
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
190
|
+
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
192
|
+
you may not use this file except in compliance with the License.
|
193
|
+
You may obtain a copy of the License at
|
194
|
+
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
196
|
+
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
200
|
+
See the License for the specific language governing permissions and
|
201
|
+
limitations under the License.
|
package/dist/index.d.ts
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
export interface XataRecord {
|
2
|
-
|
3
|
-
|
2
|
+
id: string;
|
3
|
+
xata: {
|
4
|
+
version: number;
|
5
|
+
};
|
4
6
|
read(): Promise<this>;
|
5
7
|
update(data: Selectable<this>): Promise<this>;
|
6
8
|
delete(): Promise<void>;
|
@@ -17,34 +19,40 @@ export declare type OmitLinks<T> = {
|
|
17
19
|
export declare type OmitMethods<T> = {
|
18
20
|
[key in keyof T as T[key] extends Function ? never : key]: T[key];
|
19
21
|
};
|
20
|
-
export declare type Selectable<T> = Omit<OmitQueries<OmitMethods<T>>, '
|
22
|
+
export declare type Selectable<T> = Omit<OmitQueries<OmitMethods<T>>, 'id' | 'xata'>;
|
21
23
|
export declare type Select<T, K extends keyof T> = Pick<T, K> & Queries<T> & XataRecord;
|
22
24
|
export declare type Include<T> = {
|
23
25
|
[key in keyof T as T[key] extends XataRecord ? key : never]?: boolean | Array<keyof Selectable<T[key]>>;
|
24
26
|
};
|
25
27
|
declare type SortDirection = 'asc' | 'desc';
|
26
|
-
declare type Operator = '$gt' | '$lt' | '$ge' | '$le' | '$exists' | '$notExists' | '$endsWith' | '$startsWith' | '$pattern' | '$isNot' | '$contains' | '$includes' | '$includesSubstring' | '$includesPattern' | '$includesAll';
|
27
|
-
declare type Constraint<T> =
|
28
|
+
declare type Operator = '$gt' | '$lt' | '$ge' | '$le' | '$exists' | '$notExists' | '$endsWith' | '$startsWith' | '$pattern' | '$is' | '$isNot' | '$contains' | '$includes' | '$includesSubstring' | '$includesPattern' | '$includesAll';
|
29
|
+
declare type Constraint<T> = {
|
30
|
+
[key in Operator]?: T;
|
31
|
+
};
|
32
|
+
declare type DeepConstraint<T> = T extends Record<string, any> ? {
|
33
|
+
[key in keyof T]?: T[key] | DeepConstraint<T[key]>;
|
34
|
+
} : Constraint<T>;
|
28
35
|
declare type ComparableType = number | Date;
|
29
|
-
export declare const gt: <T extends ComparableType>(value: T) =>
|
30
|
-
export declare const ge: <T extends ComparableType>(value: T) =>
|
31
|
-
export declare const gte: <T extends ComparableType>(value: T) =>
|
32
|
-
export declare const lt: <T extends ComparableType>(value: T) =>
|
33
|
-
export declare const lte: <T extends ComparableType>(value: T) =>
|
34
|
-
export declare const le: <T extends ComparableType>(value: T) =>
|
36
|
+
export declare const gt: <T extends ComparableType>(value: T) => Constraint<T>;
|
37
|
+
export declare const ge: <T extends ComparableType>(value: T) => Constraint<T>;
|
38
|
+
export declare const gte: <T extends ComparableType>(value: T) => Constraint<T>;
|
39
|
+
export declare const lt: <T extends ComparableType>(value: T) => Constraint<T>;
|
40
|
+
export declare const lte: <T extends ComparableType>(value: T) => Constraint<T>;
|
41
|
+
export declare const le: <T extends ComparableType>(value: T) => Constraint<T>;
|
35
42
|
export declare const exists: (column: string) => Constraint<string>;
|
36
43
|
export declare const notExists: (column: string) => Constraint<string>;
|
37
44
|
export declare const startsWith: (value: string) => Constraint<string>;
|
38
45
|
export declare const endsWith: (value: string) => Constraint<string>;
|
39
46
|
export declare const pattern: (value: string) => Constraint<string>;
|
40
|
-
export declare const
|
41
|
-
export declare const
|
47
|
+
export declare const is: <T>(value: T) => Constraint<T>;
|
48
|
+
export declare const isNot: <T>(value: T) => Constraint<T>;
|
49
|
+
export declare const contains: <T>(value: T) => Constraint<T>;
|
42
50
|
export declare const includes: (value: string) => Constraint<string>;
|
43
51
|
export declare const includesSubstring: (value: string) => Constraint<string>;
|
44
52
|
export declare const includesPattern: (value: string) => Constraint<string>;
|
45
53
|
export declare const includesAll: (value: string) => Constraint<string>;
|
46
54
|
declare type FilterConstraints<T> = {
|
47
|
-
[key in keyof T]?: T[key] extends Record<string, any> ? FilterConstraints<T[key]> : T[key] |
|
55
|
+
[key in keyof T]?: T[key] extends Record<string, any> ? FilterConstraints<T[key]> : T[key] | DeepConstraint<T[key]>;
|
48
56
|
};
|
49
57
|
declare type BulkQueryOptions<T> = {
|
50
58
|
filter?: FilterConstraints<T>;
|
@@ -68,7 +76,7 @@ export declare class Query<T, R = T> {
|
|
68
76
|
not(...queries: Query<T, R>[]): Query<T, R>;
|
69
77
|
none(...queries: Query<T, R>[]): Query<T, R>;
|
70
78
|
filter(constraints: FilterConstraints<T>): Query<T, R>;
|
71
|
-
filter<F extends keyof T>(column: F, value: FilterConstraints<T[F]> |
|
79
|
+
filter<F extends keyof T>(column: F, value: FilterConstraints<T[F]> | DeepConstraint<T[F]>): Query<T, R>;
|
72
80
|
sort<F extends keyof T>(column: F, direction: SortDirection): Query<T, R>;
|
73
81
|
getMany(options?: BulkQueryOptions<T>): Promise<R[]>;
|
74
82
|
getOne(options?: BulkQueryOptions<T>): Promise<R | null>;
|
@@ -101,18 +109,25 @@ interface RepositoryFactory {
|
|
101
109
|
export declare class RestRespositoryFactory implements RepositoryFactory {
|
102
110
|
createRepository<T>(client: BaseClient<any>, table: string): Repository<T>;
|
103
111
|
}
|
112
|
+
declare type BranchStrategyValue = string | undefined | null;
|
113
|
+
declare type BranchStrategyBuilder = () => BranchStrategyValue | Promise<BranchStrategyValue>;
|
114
|
+
declare type BranchStrategy = BranchStrategyValue | BranchStrategyBuilder;
|
115
|
+
declare type BranchStrategyOption = NonNullable<BranchStrategy | BranchStrategy[]>;
|
104
116
|
export declare type XataClientOptions = {
|
105
117
|
fetch?: unknown;
|
106
118
|
databaseURL: string;
|
119
|
+
branch: BranchStrategyOption;
|
107
120
|
apiKey: string;
|
108
121
|
repositoryFactory?: RepositoryFactory;
|
109
122
|
};
|
110
123
|
export declare class BaseClient<D extends Record<string, Repository<any>>> {
|
111
124
|
options: XataClientOptions;
|
112
125
|
private links;
|
126
|
+
private branch;
|
113
127
|
db: D;
|
114
128
|
constructor(options: XataClientOptions, links: Links);
|
115
129
|
initObject<T>(table: string, object: object): T;
|
130
|
+
getBranch(): Promise<string>;
|
116
131
|
}
|
117
132
|
export declare class XataError extends Error {
|
118
133
|
readonly status: number;
|
package/dist/index.js
CHANGED
@@ -8,8 +8,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
9
|
});
|
10
10
|
};
|
11
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
12
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
13
|
+
var m = o[Symbol.asyncIterator], i;
|
14
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
15
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
16
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
17
|
+
};
|
11
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
12
|
-
exports.XataError = exports.BaseClient = exports.RestRespositoryFactory = exports.RestRepository = exports.Repository = exports.Query = exports.includesAll = exports.includesPattern = exports.includesSubstring = exports.includes = exports.contains = exports.isNot = exports.pattern = exports.endsWith = exports.startsWith = exports.notExists = exports.exists = exports.le = exports.lte = exports.lt = exports.gte = exports.ge = exports.gt = void 0;
|
19
|
+
exports.XataError = exports.BaseClient = exports.RestRespositoryFactory = exports.RestRepository = exports.Repository = exports.Query = exports.includesAll = exports.includesPattern = exports.includesSubstring = exports.includes = exports.contains = exports.isNot = exports.is = exports.pattern = exports.endsWith = exports.startsWith = exports.notExists = exports.exists = exports.le = exports.lte = exports.lt = exports.gte = exports.ge = exports.gt = void 0;
|
13
20
|
const gt = (value) => ({ $gt: value });
|
14
21
|
exports.gt = gt;
|
15
22
|
const ge = (value) => ({ $ge: value });
|
@@ -32,14 +39,11 @@ const endsWith = (value) => ({ $endsWith: value });
|
|
32
39
|
exports.endsWith = endsWith;
|
33
40
|
const pattern = (value) => ({ $pattern: value });
|
34
41
|
exports.pattern = pattern;
|
42
|
+
const is = (value) => ({ $is: value });
|
43
|
+
exports.is = is;
|
35
44
|
const isNot = (value) => ({ $isNot: value });
|
36
45
|
exports.isNot = isNot;
|
37
|
-
const contains = (value) => {
|
38
|
-
// if (Array.isArray(value)) {
|
39
|
-
// return { $all: value.map(item => ({ $contains: item as string })) }
|
40
|
-
// }
|
41
|
-
return { $contains: value };
|
42
|
-
};
|
46
|
+
const contains = (value) => ({ $contains: value });
|
43
47
|
exports.contains = contains;
|
44
48
|
// TODO: these can only be applied to columns of type "multiple"
|
45
49
|
const includes = (value) => ({ $includes: value });
|
@@ -187,13 +191,14 @@ class RestRepository extends Repository {
|
|
187
191
|
}
|
188
192
|
request(method, path, body) {
|
189
193
|
return __awaiter(this, void 0, void 0, function* () {
|
190
|
-
const { databaseURL } = this.client.options;
|
191
|
-
const
|
194
|
+
const { databaseURL, apiKey } = this.client.options;
|
195
|
+
const branch = yield this.client.getBranch();
|
196
|
+
const resp = yield this.fetch(`${databaseURL}:${branch}${path}`, {
|
192
197
|
method,
|
193
198
|
headers: {
|
194
199
|
Accept: '*/*',
|
195
200
|
'Content-Type': 'application/json',
|
196
|
-
Authorization: `Bearer ${
|
201
|
+
Authorization: `Bearer ${apiKey}`
|
197
202
|
},
|
198
203
|
body: JSON.stringify(body)
|
199
204
|
});
|
@@ -226,8 +231,8 @@ class RestRepository extends Repository {
|
|
226
231
|
const body = Object.assign({}, object);
|
227
232
|
for (const key of Object.keys(body)) {
|
228
233
|
const value = body[key];
|
229
|
-
if (value && typeof value === 'object' && typeof value.
|
230
|
-
body[key] = value.
|
234
|
+
if (value && typeof value === 'object' && typeof value.id === 'string') {
|
235
|
+
body[key] = value.id;
|
231
236
|
}
|
232
237
|
}
|
233
238
|
const obj = yield this.request('POST', `/tables/${this.table}/data`, body);
|
@@ -284,6 +289,9 @@ class RestRespositoryFactory {
|
|
284
289
|
exports.RestRespositoryFactory = RestRespositoryFactory;
|
285
290
|
class BaseClient {
|
286
291
|
constructor(options, links) {
|
292
|
+
if (!options.databaseURL || !options.apiKey || !options.branch) {
|
293
|
+
throw new Error('Options databaseURL, apiKey and branch are required');
|
294
|
+
}
|
287
295
|
this.options = options;
|
288
296
|
this.links = links;
|
289
297
|
}
|
@@ -295,15 +303,15 @@ class BaseClient {
|
|
295
303
|
const [field, linkTable] = link;
|
296
304
|
const value = o[field];
|
297
305
|
if (value && typeof value === 'object') {
|
298
|
-
const {
|
299
|
-
if (Object.keys(value).find((col) =>
|
306
|
+
const { id } = value;
|
307
|
+
if (Object.keys(value).find((col) => col === 'id')) {
|
300
308
|
o[field] = this.initObject(linkTable, value);
|
301
309
|
}
|
302
|
-
else if (
|
310
|
+
else if (id) {
|
303
311
|
o[field] = {
|
304
|
-
|
312
|
+
id,
|
305
313
|
get: () => {
|
306
|
-
this.db[linkTable].read(
|
314
|
+
this.db[linkTable].read(id);
|
307
315
|
}
|
308
316
|
};
|
309
317
|
}
|
@@ -311,13 +319,13 @@ class BaseClient {
|
|
311
319
|
}
|
312
320
|
const db = this.db;
|
313
321
|
o.read = function () {
|
314
|
-
return db[table].read(o['
|
322
|
+
return db[table].read(o['id']);
|
315
323
|
};
|
316
324
|
o.update = function (data) {
|
317
|
-
return db[table].update(o['
|
325
|
+
return db[table].update(o['id'], data);
|
318
326
|
};
|
319
327
|
o.delete = function () {
|
320
|
-
return db[table].delete(o['
|
328
|
+
return db[table].delete(o['id']);
|
321
329
|
};
|
322
330
|
for (const prop of ['read', 'update', 'delete']) {
|
323
331
|
Object.defineProperty(o, prop, { enumerable: false });
|
@@ -326,6 +334,36 @@ class BaseClient {
|
|
326
334
|
Object.freeze(o);
|
327
335
|
return o;
|
328
336
|
}
|
337
|
+
getBranch() {
|
338
|
+
var e_1, _a;
|
339
|
+
return __awaiter(this, void 0, void 0, function* () {
|
340
|
+
if (this.branch)
|
341
|
+
return this.branch;
|
342
|
+
const { branch: param } = this.options;
|
343
|
+
const strategies = Array.isArray(param) ? [...param] : [param];
|
344
|
+
const evaluateBranch = (strategy) => __awaiter(this, void 0, void 0, function* () {
|
345
|
+
return isBranchStrategyBuilder(strategy) ? yield strategy() : strategy;
|
346
|
+
});
|
347
|
+
try {
|
348
|
+
for (var strategies_1 = __asyncValues(strategies), strategies_1_1; strategies_1_1 = yield strategies_1.next(), !strategies_1_1.done;) {
|
349
|
+
const strategy = strategies_1_1.value;
|
350
|
+
const branch = yield evaluateBranch(strategy);
|
351
|
+
if (branch) {
|
352
|
+
this.branch = branch;
|
353
|
+
return branch;
|
354
|
+
}
|
355
|
+
}
|
356
|
+
}
|
357
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
358
|
+
finally {
|
359
|
+
try {
|
360
|
+
if (strategies_1_1 && !strategies_1_1.done && (_a = strategies_1.return)) yield _a.call(strategies_1);
|
361
|
+
}
|
362
|
+
finally { if (e_1) throw e_1.error; }
|
363
|
+
}
|
364
|
+
throw new Error('Unable to resolve branch value');
|
365
|
+
});
|
366
|
+
}
|
329
367
|
}
|
330
368
|
exports.BaseClient = BaseClient;
|
331
369
|
class XataError extends Error {
|
@@ -335,3 +373,6 @@ class XataError extends Error {
|
|
335
373
|
}
|
336
374
|
}
|
337
375
|
exports.XataError = XataError;
|
376
|
+
const isBranchStrategyBuilder = (strategy) => {
|
377
|
+
return typeof strategy === 'function';
|
378
|
+
};
|
package/dist/index.test.js
CHANGED
@@ -10,22 +10,132 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
10
10
|
};
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
12
12
|
const _1 = require("./");
|
13
|
-
const
|
14
|
-
const
|
15
|
-
fetch
|
16
|
-
apiKey
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
const buildClient = (options = {}) => {
|
14
|
+
const { apiKey = '1234', databaseURL = 'https://my-workspace-5df34do.staging.xatabase.co/db/xata', branch = 'main' } = options;
|
15
|
+
const fetch = jest.fn();
|
16
|
+
const client = new _1.BaseClient({ fetch, apiKey, databaseURL, branch }, {});
|
17
|
+
const users = new _1.RestRepository(client, 'users');
|
18
|
+
return { fetch, client, users };
|
19
|
+
};
|
20
|
+
describe('client options', () => {
|
21
|
+
test('option parameters are set', () => {
|
22
|
+
const { client } = buildClient({ apiKey: 'apiKey', databaseURL: 'url' });
|
23
|
+
expect(client.options.apiKey).toBe('apiKey');
|
24
|
+
expect(client.options.databaseURL).toBe('url');
|
25
|
+
});
|
26
|
+
test('throws if mandatory options are missing', () => {
|
27
|
+
// @ts-expect-error Options are mandatory in TypeScript
|
28
|
+
expect(() => buildClient({ apiKey: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
|
29
|
+
// @ts-expect-error Options are mandatory in TypeScript
|
30
|
+
expect(() => buildClient({ databaseURL: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
|
31
|
+
// @ts-expect-error Options are mandatory in TypeScript
|
32
|
+
expect(() => buildClient({ branch: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
|
33
|
+
});
|
34
|
+
test('throws if branch cannot be resolved', () => {
|
35
|
+
const { users } = buildClient({ branch: () => null });
|
36
|
+
expect(users.request('GET', '/foo')).rejects.toThrow('Unable to resolve branch value');
|
37
|
+
});
|
38
|
+
test('provide branch as a string', () => __awaiter(void 0, void 0, void 0, function* () {
|
39
|
+
const { fetch, users } = buildClient({ branch: 'branch' });
|
40
|
+
fetch.mockReset().mockImplementation(() => {
|
41
|
+
return {
|
42
|
+
ok: true,
|
43
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
|
44
|
+
};
|
45
|
+
});
|
46
|
+
yield users.request('GET', '/foo');
|
47
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
48
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
49
|
+
Array [
|
50
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
|
51
|
+
Object {
|
52
|
+
"body": undefined,
|
53
|
+
"headers": Object {
|
54
|
+
"Accept": "*/*",
|
55
|
+
"Authorization": "Bearer 1234",
|
56
|
+
"Content-Type": "application/json",
|
57
|
+
},
|
58
|
+
"method": "GET",
|
59
|
+
},
|
60
|
+
]
|
61
|
+
`);
|
62
|
+
}));
|
63
|
+
test('provide branch as an array', () => __awaiter(void 0, void 0, void 0, function* () {
|
64
|
+
const { fetch, users } = buildClient({
|
65
|
+
branch: [process.env.NOT_DEFINED_VARIABLE, () => __awaiter(void 0, void 0, void 0, function* () { return null; }), 'branch', 'main']
|
66
|
+
});
|
67
|
+
fetch.mockReset().mockImplementation(() => {
|
68
|
+
return {
|
69
|
+
ok: true,
|
70
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
|
71
|
+
};
|
72
|
+
});
|
73
|
+
yield users.request('GET', '/foo');
|
74
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
75
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
76
|
+
Array [
|
77
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
|
78
|
+
Object {
|
79
|
+
"body": undefined,
|
80
|
+
"headers": Object {
|
81
|
+
"Accept": "*/*",
|
82
|
+
"Authorization": "Bearer 1234",
|
83
|
+
"Content-Type": "application/json",
|
84
|
+
},
|
85
|
+
"method": "GET",
|
86
|
+
},
|
87
|
+
]
|
88
|
+
`);
|
89
|
+
}));
|
90
|
+
test('provide branch as a function', () => __awaiter(void 0, void 0, void 0, function* () {
|
91
|
+
const { fetch, users } = buildClient({ branch: () => 'branch' });
|
92
|
+
fetch.mockReset().mockImplementation(() => {
|
93
|
+
return {
|
94
|
+
ok: true,
|
95
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
|
96
|
+
};
|
97
|
+
});
|
98
|
+
yield users.request('GET', '/foo');
|
99
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
100
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
101
|
+
Array [
|
102
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
|
103
|
+
Object {
|
104
|
+
"body": undefined,
|
105
|
+
"headers": Object {
|
106
|
+
"Accept": "*/*",
|
107
|
+
"Authorization": "Bearer 1234",
|
108
|
+
"Content-Type": "application/json",
|
109
|
+
},
|
110
|
+
"method": "GET",
|
111
|
+
},
|
112
|
+
]
|
113
|
+
`);
|
114
|
+
}));
|
115
|
+
test('ensure branch resolution is memoized', () => __awaiter(void 0, void 0, void 0, function* () {
|
116
|
+
const branchGetter = jest.fn(() => 'branch');
|
117
|
+
const { fetch, users } = buildClient({ branch: branchGetter });
|
118
|
+
fetch.mockReset().mockImplementation(() => {
|
119
|
+
return {
|
120
|
+
ok: true,
|
121
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
|
122
|
+
};
|
123
|
+
});
|
124
|
+
yield users.request('GET', '/foo');
|
125
|
+
yield users.request('GET', '/foo');
|
126
|
+
expect(branchGetter).toHaveBeenCalledTimes(1);
|
127
|
+
}));
|
128
|
+
});
|
20
129
|
describe('request', () => {
|
21
130
|
test('builds the right arguments for a GET request', () => __awaiter(void 0, void 0, void 0, function* () {
|
131
|
+
const { fetch, users } = buildClient();
|
22
132
|
fetch.mockReset().mockImplementation(() => {
|
23
133
|
return {
|
24
134
|
ok: true,
|
25
135
|
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
|
26
136
|
};
|
27
137
|
});
|
28
|
-
users.request('GET', '/foo');
|
138
|
+
yield users.request('GET', '/foo');
|
29
139
|
expect(fetch).toHaveBeenCalledTimes(1);
|
30
140
|
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
31
141
|
Array [
|
@@ -43,13 +153,14 @@ describe('request', () => {
|
|
43
153
|
`);
|
44
154
|
}));
|
45
155
|
test('builds the right arguments for a POST request', () => __awaiter(void 0, void 0, void 0, function* () {
|
156
|
+
const { fetch, users } = buildClient();
|
46
157
|
fetch.mockReset().mockImplementation(() => {
|
47
158
|
return {
|
48
159
|
ok: true,
|
49
160
|
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
|
50
161
|
};
|
51
162
|
});
|
52
|
-
users.request('POST', '/foo', { a: 1 });
|
163
|
+
yield users.request('POST', '/foo', { a: 1 });
|
53
164
|
expect(fetch).toHaveBeenCalledTimes(1);
|
54
165
|
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
55
166
|
Array [
|
@@ -67,6 +178,7 @@ describe('request', () => {
|
|
67
178
|
`);
|
68
179
|
}));
|
69
180
|
test('throws if the response is not ok', () => __awaiter(void 0, void 0, void 0, function* () {
|
181
|
+
const { fetch, users } = buildClient();
|
70
182
|
fetch.mockImplementation(() => {
|
71
183
|
return {
|
72
184
|
ok: false,
|
@@ -77,6 +189,7 @@ describe('request', () => {
|
|
77
189
|
expect(users.request('GET', '/foo')).rejects.toThrow(new _1.XataError('Not Found', 404));
|
78
190
|
}));
|
79
191
|
test('throws with the error from the server if the response is not ok', () => __awaiter(void 0, void 0, void 0, function* () {
|
192
|
+
const { fetch, users } = buildClient();
|
80
193
|
fetch.mockImplementation(() => {
|
81
194
|
return {
|
82
195
|
ok: false,
|
@@ -88,6 +201,7 @@ describe('request', () => {
|
|
88
201
|
expect(users.request('GET', '/foo')).rejects.toThrow(new _1.XataError('Resource not found', 404));
|
89
202
|
}));
|
90
203
|
test('returns the json body if the response is ok', () => __awaiter(void 0, void 0, void 0, function* () {
|
204
|
+
const { fetch, users } = buildClient();
|
91
205
|
const json = { a: 1 };
|
92
206
|
fetch.mockImplementation(() => {
|
93
207
|
return {
|
@@ -99,7 +213,7 @@ describe('request', () => {
|
|
99
213
|
expect(result).toEqual(json);
|
100
214
|
}));
|
101
215
|
});
|
102
|
-
function expectRequest(expectedRequest, callback, response) {
|
216
|
+
function expectRequest(users, expectedRequest, callback, response) {
|
103
217
|
return __awaiter(this, void 0, void 0, function* () {
|
104
218
|
const request = jest.fn(() => __awaiter(this, void 0, void 0, function* () { return response; }));
|
105
219
|
users.request = request;
|
@@ -115,27 +229,31 @@ function expectRequest(expectedRequest, callback, response) {
|
|
115
229
|
describe('query', () => {
|
116
230
|
describe('getMany', () => {
|
117
231
|
test('simple query', () => __awaiter(void 0, void 0, void 0, function* () {
|
232
|
+
const { users } = buildClient();
|
118
233
|
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
119
|
-
expectRequest(expected, () => users.getMany(), { records: [] });
|
234
|
+
expectRequest(users, expected, () => users.getMany(), { records: [] });
|
120
235
|
}));
|
121
236
|
test('query with one filter', () => __awaiter(void 0, void 0, void 0, function* () {
|
237
|
+
const { users } = buildClient();
|
122
238
|
const expected = { method: 'POST', path: '/tables/users/query', body: { filter: { $all: [{ name: 'foo' }] } } };
|
123
|
-
expectRequest(expected, () => users.filter('name', 'foo').getMany(), { records: [] });
|
239
|
+
expectRequest(users, expected, () => users.filter('name', 'foo').getMany(), { records: [] });
|
124
240
|
}));
|
125
241
|
});
|
126
242
|
describe('getOne', () => {
|
127
243
|
test('returns a single object', () => __awaiter(void 0, void 0, void 0, function* () {
|
128
|
-
const
|
244
|
+
const { users } = buildClient();
|
245
|
+
const result = { records: [{ id: '1234' }] };
|
129
246
|
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
130
|
-
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
247
|
+
expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
131
248
|
const first = yield users.select().getOne();
|
132
|
-
expect(first === null || first === void 0 ? void 0 : first.
|
249
|
+
expect(first === null || first === void 0 ? void 0 : first.id).toBe(result.records[0].id);
|
133
250
|
}), result);
|
134
251
|
}));
|
135
252
|
test('returns null if no objects are returned', () => __awaiter(void 0, void 0, void 0, function* () {
|
253
|
+
const { users } = buildClient();
|
136
254
|
const result = { records: [] };
|
137
255
|
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
138
|
-
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
256
|
+
expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
139
257
|
const first = yield users.getOne();
|
140
258
|
expect(first).toBeNull();
|
141
259
|
}), result);
|
@@ -144,26 +262,29 @@ describe('query', () => {
|
|
144
262
|
});
|
145
263
|
describe('read', () => {
|
146
264
|
test('reads an object by id successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
265
|
+
const { users } = buildClient();
|
147
266
|
const id = 'rec_1234';
|
148
267
|
const expected = { method: 'GET', path: `/tables/users/data/${id}`, body: undefined };
|
149
|
-
expectRequest(expected, () => users.read(id));
|
268
|
+
expectRequest(users, expected, () => users.read(id));
|
150
269
|
}));
|
151
270
|
});
|
152
271
|
describe('Repository.update', () => {
|
153
272
|
test('updates and object successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
154
|
-
const
|
155
|
-
const
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
273
|
+
const { users } = buildClient();
|
274
|
+
const object = { id: 'rec_1234', xata: { version: 1 }, name: 'Ada' };
|
275
|
+
const expected = { method: 'PUT', path: `/tables/users/data/${object.id}`, body: object };
|
276
|
+
expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
277
|
+
const result = yield users.update(object.id, object);
|
278
|
+
expect(result.id).toBe(object.id);
|
279
|
+
}), { id: object.id });
|
160
280
|
}));
|
161
281
|
});
|
162
282
|
describe('Repository.delete', () => {
|
163
283
|
test('deletes a record by id successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
284
|
+
const { users } = buildClient();
|
164
285
|
const id = 'rec_1234';
|
165
286
|
const expected = { method: 'DELETE', path: `/tables/users/data/${id}`, body: undefined };
|
166
|
-
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
287
|
+
expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
167
288
|
const result = yield users.delete(id);
|
168
289
|
expect(result).toBe(undefined);
|
169
290
|
}));
|
@@ -171,12 +292,13 @@ describe('Repository.delete', () => {
|
|
171
292
|
});
|
172
293
|
describe('create', () => {
|
173
294
|
test('successful', () => __awaiter(void 0, void 0, void 0, function* () {
|
174
|
-
const
|
295
|
+
const { users } = buildClient();
|
296
|
+
const created = { id: 'rec_1234', _version: 0 };
|
175
297
|
const object = { name: 'Ada' };
|
176
298
|
const expected = { method: 'POST', path: '/tables/users/data', body: object };
|
177
|
-
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
299
|
+
expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
178
300
|
const result = yield users.create(object);
|
179
|
-
expect(result.
|
301
|
+
expect(result.id).toBe(created.id);
|
180
302
|
}), created);
|
181
303
|
}));
|
182
304
|
});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@xata.io/client",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.2.2",
|
4
4
|
"description": "Xata.io SDK for TypeScript and JavaScript",
|
5
5
|
"main": "./dist/index.js",
|
6
6
|
"types": "./dist/index.d.ts",
|
@@ -19,5 +19,6 @@
|
|
19
19
|
"bugs": {
|
20
20
|
"url": "https://github.com/xataio/client-ts/issues"
|
21
21
|
},
|
22
|
-
"homepage": "https://github.com/xataio/client-ts
|
22
|
+
"homepage": "https://github.com/xataio/client-ts/blob/main/client/README.md",
|
23
|
+
"gitHead": "e5103cd1b370d60fd8c8a70841352037d88c816d"
|
23
24
|
}
|
package/src/index.test.ts
CHANGED
@@ -1,23 +1,159 @@
|
|
1
|
-
import { BaseClient, RestRepository, XataError, XataRecord } from './';
|
2
|
-
|
3
|
-
const fetch = jest.fn();
|
4
|
-
const client = new BaseClient(
|
5
|
-
{
|
6
|
-
fetch,
|
7
|
-
apiKey: '1234',
|
8
|
-
databaseURL: 'https://my-workspace-5df34do.staging.xatabase.co/db/xata:main'
|
9
|
-
},
|
10
|
-
{}
|
11
|
-
);
|
1
|
+
import { BaseClient, RestRepository, XataClientOptions, XataError, XataRecord } from './';
|
12
2
|
|
13
3
|
interface User extends XataRecord {
|
14
4
|
name: string;
|
15
5
|
}
|
16
6
|
|
17
|
-
const
|
7
|
+
const buildClient = (options: Partial<XataClientOptions> = {}) => {
|
8
|
+
const {
|
9
|
+
apiKey = '1234',
|
10
|
+
databaseURL = 'https://my-workspace-5df34do.staging.xatabase.co/db/xata',
|
11
|
+
branch = 'main'
|
12
|
+
} = options;
|
13
|
+
|
14
|
+
const fetch = jest.fn();
|
15
|
+
const client = new BaseClient({ fetch, apiKey, databaseURL, branch }, {});
|
16
|
+
const users = new RestRepository<User>(client, 'users');
|
17
|
+
|
18
|
+
return { fetch, client, users };
|
19
|
+
};
|
20
|
+
|
21
|
+
describe('client options', () => {
|
22
|
+
test('option parameters are set', () => {
|
23
|
+
const { client } = buildClient({ apiKey: 'apiKey', databaseURL: 'url' });
|
24
|
+
expect(client.options.apiKey).toBe('apiKey');
|
25
|
+
expect(client.options.databaseURL).toBe('url');
|
26
|
+
});
|
27
|
+
|
28
|
+
test('throws if mandatory options are missing', () => {
|
29
|
+
// @ts-expect-error Options are mandatory in TypeScript
|
30
|
+
expect(() => buildClient({ apiKey: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
|
31
|
+
|
32
|
+
// @ts-expect-error Options are mandatory in TypeScript
|
33
|
+
expect(() => buildClient({ databaseURL: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
|
34
|
+
|
35
|
+
// @ts-expect-error Options are mandatory in TypeScript
|
36
|
+
expect(() => buildClient({ branch: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
|
37
|
+
});
|
38
|
+
|
39
|
+
test('throws if branch cannot be resolved', () => {
|
40
|
+
const { users } = buildClient({ branch: () => null });
|
41
|
+
|
42
|
+
expect(users.request('GET', '/foo')).rejects.toThrow('Unable to resolve branch value');
|
43
|
+
});
|
44
|
+
|
45
|
+
test('provide branch as a string', async () => {
|
46
|
+
const { fetch, users } = buildClient({ branch: 'branch' });
|
47
|
+
|
48
|
+
fetch.mockReset().mockImplementation(() => {
|
49
|
+
return {
|
50
|
+
ok: true,
|
51
|
+
json: async () => ({})
|
52
|
+
};
|
53
|
+
});
|
54
|
+
|
55
|
+
await users.request('GET', '/foo');
|
56
|
+
|
57
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
58
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
59
|
+
Array [
|
60
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
|
61
|
+
Object {
|
62
|
+
"body": undefined,
|
63
|
+
"headers": Object {
|
64
|
+
"Accept": "*/*",
|
65
|
+
"Authorization": "Bearer 1234",
|
66
|
+
"Content-Type": "application/json",
|
67
|
+
},
|
68
|
+
"method": "GET",
|
69
|
+
},
|
70
|
+
]
|
71
|
+
`);
|
72
|
+
});
|
73
|
+
|
74
|
+
test('provide branch as an array', async () => {
|
75
|
+
const { fetch, users } = buildClient({
|
76
|
+
branch: [process.env.NOT_DEFINED_VARIABLE, async () => null, 'branch', 'main']
|
77
|
+
});
|
78
|
+
|
79
|
+
fetch.mockReset().mockImplementation(() => {
|
80
|
+
return {
|
81
|
+
ok: true,
|
82
|
+
json: async () => ({})
|
83
|
+
};
|
84
|
+
});
|
85
|
+
|
86
|
+
await users.request('GET', '/foo');
|
87
|
+
|
88
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
89
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
90
|
+
Array [
|
91
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
|
92
|
+
Object {
|
93
|
+
"body": undefined,
|
94
|
+
"headers": Object {
|
95
|
+
"Accept": "*/*",
|
96
|
+
"Authorization": "Bearer 1234",
|
97
|
+
"Content-Type": "application/json",
|
98
|
+
},
|
99
|
+
"method": "GET",
|
100
|
+
},
|
101
|
+
]
|
102
|
+
`);
|
103
|
+
});
|
104
|
+
|
105
|
+
test('provide branch as a function', async () => {
|
106
|
+
const { fetch, users } = buildClient({ branch: () => 'branch' });
|
107
|
+
|
108
|
+
fetch.mockReset().mockImplementation(() => {
|
109
|
+
return {
|
110
|
+
ok: true,
|
111
|
+
json: async () => ({})
|
112
|
+
};
|
113
|
+
});
|
114
|
+
|
115
|
+
await users.request('GET', '/foo');
|
116
|
+
|
117
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
118
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
119
|
+
Array [
|
120
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
|
121
|
+
Object {
|
122
|
+
"body": undefined,
|
123
|
+
"headers": Object {
|
124
|
+
"Accept": "*/*",
|
125
|
+
"Authorization": "Bearer 1234",
|
126
|
+
"Content-Type": "application/json",
|
127
|
+
},
|
128
|
+
"method": "GET",
|
129
|
+
},
|
130
|
+
]
|
131
|
+
`);
|
132
|
+
});
|
133
|
+
|
134
|
+
test('ensure branch resolution is memoized', async () => {
|
135
|
+
const branchGetter = jest.fn(() => 'branch');
|
136
|
+
|
137
|
+
const { fetch, users } = buildClient({ branch: branchGetter });
|
138
|
+
|
139
|
+
fetch.mockReset().mockImplementation(() => {
|
140
|
+
return {
|
141
|
+
ok: true,
|
142
|
+
json: async () => ({})
|
143
|
+
};
|
144
|
+
});
|
145
|
+
|
146
|
+
await users.request('GET', '/foo');
|
147
|
+
await users.request('GET', '/foo');
|
148
|
+
|
149
|
+
expect(branchGetter).toHaveBeenCalledTimes(1);
|
150
|
+
});
|
151
|
+
});
|
18
152
|
|
19
153
|
describe('request', () => {
|
20
154
|
test('builds the right arguments for a GET request', async () => {
|
155
|
+
const { fetch, users } = buildClient();
|
156
|
+
|
21
157
|
fetch.mockReset().mockImplementation(() => {
|
22
158
|
return {
|
23
159
|
ok: true,
|
@@ -25,7 +161,7 @@ describe('request', () => {
|
|
25
161
|
};
|
26
162
|
});
|
27
163
|
|
28
|
-
users.request('GET', '/foo');
|
164
|
+
await users.request('GET', '/foo');
|
29
165
|
|
30
166
|
expect(fetch).toHaveBeenCalledTimes(1);
|
31
167
|
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
@@ -45,6 +181,8 @@ describe('request', () => {
|
|
45
181
|
});
|
46
182
|
|
47
183
|
test('builds the right arguments for a POST request', async () => {
|
184
|
+
const { fetch, users } = buildClient();
|
185
|
+
|
48
186
|
fetch.mockReset().mockImplementation(() => {
|
49
187
|
return {
|
50
188
|
ok: true,
|
@@ -52,7 +190,7 @@ describe('request', () => {
|
|
52
190
|
};
|
53
191
|
});
|
54
192
|
|
55
|
-
users.request('POST', '/foo', { a: 1 });
|
193
|
+
await users.request('POST', '/foo', { a: 1 });
|
56
194
|
|
57
195
|
expect(fetch).toHaveBeenCalledTimes(1);
|
58
196
|
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
@@ -72,6 +210,8 @@ describe('request', () => {
|
|
72
210
|
});
|
73
211
|
|
74
212
|
test('throws if the response is not ok', async () => {
|
213
|
+
const { fetch, users } = buildClient();
|
214
|
+
|
75
215
|
fetch.mockImplementation(() => {
|
76
216
|
return {
|
77
217
|
ok: false,
|
@@ -84,6 +224,8 @@ describe('request', () => {
|
|
84
224
|
});
|
85
225
|
|
86
226
|
test('throws with the error from the server if the response is not ok', async () => {
|
227
|
+
const { fetch, users } = buildClient();
|
228
|
+
|
87
229
|
fetch.mockImplementation(() => {
|
88
230
|
return {
|
89
231
|
ok: false,
|
@@ -97,6 +239,8 @@ describe('request', () => {
|
|
97
239
|
});
|
98
240
|
|
99
241
|
test('returns the json body if the response is ok', async () => {
|
242
|
+
const { fetch, users } = buildClient();
|
243
|
+
|
100
244
|
const json = { a: 1 };
|
101
245
|
fetch.mockImplementation(() => {
|
102
246
|
return {
|
@@ -116,7 +260,12 @@ type ExpectedRequest = {
|
|
116
260
|
body: unknown;
|
117
261
|
};
|
118
262
|
|
119
|
-
async function expectRequest(
|
263
|
+
async function expectRequest(
|
264
|
+
users: RestRepository<User>,
|
265
|
+
expectedRequest: ExpectedRequest,
|
266
|
+
callback: () => void,
|
267
|
+
response?: unknown
|
268
|
+
) {
|
120
269
|
const request = jest.fn(async () => response);
|
121
270
|
users.request = request;
|
122
271
|
|
@@ -133,34 +282,44 @@ async function expectRequest(expectedRequest: ExpectedRequest, callback: () => v
|
|
133
282
|
describe('query', () => {
|
134
283
|
describe('getMany', () => {
|
135
284
|
test('simple query', async () => {
|
285
|
+
const { users } = buildClient();
|
286
|
+
|
136
287
|
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
137
|
-
expectRequest(expected, () => users.getMany(), { records: [] });
|
288
|
+
expectRequest(users, expected, () => users.getMany(), { records: [] });
|
138
289
|
});
|
139
290
|
|
140
291
|
test('query with one filter', async () => {
|
292
|
+
const { users } = buildClient();
|
293
|
+
|
141
294
|
const expected = { method: 'POST', path: '/tables/users/query', body: { filter: { $all: [{ name: 'foo' }] } } };
|
142
|
-
expectRequest(expected, () => users.filter('name', 'foo').getMany(), { records: [] });
|
295
|
+
expectRequest(users, expected, () => users.filter('name', 'foo').getMany(), { records: [] });
|
143
296
|
});
|
144
297
|
});
|
145
298
|
|
146
299
|
describe('getOne', () => {
|
147
300
|
test('returns a single object', async () => {
|
148
|
-
const
|
301
|
+
const { users } = buildClient();
|
302
|
+
|
303
|
+
const result = { records: [{ id: '1234' }] };
|
149
304
|
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
150
305
|
expectRequest(
|
306
|
+
users,
|
151
307
|
expected,
|
152
308
|
async () => {
|
153
309
|
const first = await users.select().getOne();
|
154
|
-
expect(first?.
|
310
|
+
expect(first?.id).toBe(result.records[0].id);
|
155
311
|
},
|
156
312
|
result
|
157
313
|
);
|
158
314
|
});
|
159
315
|
|
160
316
|
test('returns null if no objects are returned', async () => {
|
317
|
+
const { users } = buildClient();
|
318
|
+
|
161
319
|
const result = { records: [] };
|
162
320
|
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
163
321
|
expectRequest(
|
322
|
+
users,
|
164
323
|
expected,
|
165
324
|
async () => {
|
166
325
|
const first = await users.getOne();
|
@@ -174,31 +333,39 @@ describe('query', () => {
|
|
174
333
|
|
175
334
|
describe('read', () => {
|
176
335
|
test('reads an object by id successfully', async () => {
|
336
|
+
const { users } = buildClient();
|
337
|
+
|
177
338
|
const id = 'rec_1234';
|
178
339
|
const expected = { method: 'GET', path: `/tables/users/data/${id}`, body: undefined };
|
179
|
-
expectRequest(expected, () => users.read(id));
|
340
|
+
expectRequest(users, expected, () => users.read(id));
|
180
341
|
});
|
181
342
|
});
|
182
343
|
|
183
344
|
describe('Repository.update', () => {
|
184
345
|
test('updates and object successfully', async () => {
|
185
|
-
const
|
186
|
-
|
346
|
+
const { users } = buildClient();
|
347
|
+
|
348
|
+
const object = { id: 'rec_1234', xata: { version: 1 }, name: 'Ada' } as User;
|
349
|
+
const expected = { method: 'PUT', path: `/tables/users/data/${object.id}`, body: object };
|
187
350
|
expectRequest(
|
351
|
+
users,
|
188
352
|
expected,
|
189
353
|
async () => {
|
190
|
-
const result = await users.update(object.
|
191
|
-
expect(result.
|
354
|
+
const result = await users.update(object.id, object);
|
355
|
+
expect(result.id).toBe(object.id);
|
192
356
|
},
|
193
|
-
{
|
357
|
+
{ id: object.id }
|
194
358
|
);
|
195
359
|
});
|
196
360
|
});
|
361
|
+
|
197
362
|
describe('Repository.delete', () => {
|
198
363
|
test('deletes a record by id successfully', async () => {
|
364
|
+
const { users } = buildClient();
|
365
|
+
|
199
366
|
const id = 'rec_1234';
|
200
367
|
const expected = { method: 'DELETE', path: `/tables/users/data/${id}`, body: undefined };
|
201
|
-
expectRequest(expected, async () => {
|
368
|
+
expectRequest(users, expected, async () => {
|
202
369
|
const result = await users.delete(id);
|
203
370
|
expect(result).toBe(undefined);
|
204
371
|
});
|
@@ -207,14 +374,17 @@ describe('Repository.delete', () => {
|
|
207
374
|
|
208
375
|
describe('create', () => {
|
209
376
|
test('successful', async () => {
|
210
|
-
const
|
377
|
+
const { users } = buildClient();
|
378
|
+
|
379
|
+
const created = { id: 'rec_1234', _version: 0 };
|
211
380
|
const object = { name: 'Ada' } as User;
|
212
381
|
const expected = { method: 'POST', path: '/tables/users/data', body: object };
|
213
382
|
expectRequest(
|
383
|
+
users,
|
214
384
|
expected,
|
215
385
|
async () => {
|
216
386
|
const result = await users.create(object);
|
217
|
-
expect(result.
|
387
|
+
expect(result.id).toBe(created.id);
|
218
388
|
},
|
219
389
|
created
|
220
390
|
);
|
package/src/index.ts
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
export interface XataRecord {
|
2
|
-
|
3
|
-
|
2
|
+
id: string;
|
3
|
+
xata: {
|
4
|
+
version: number;
|
5
|
+
};
|
4
6
|
read(): Promise<this>;
|
5
7
|
update(data: Selectable<this>): Promise<this>;
|
6
8
|
delete(): Promise<void>;
|
@@ -23,7 +25,7 @@ export type OmitMethods<T> = {
|
|
23
25
|
[key in keyof T as T[key] extends Function ? never : key]: T[key];
|
24
26
|
};
|
25
27
|
|
26
|
-
export type Selectable<T> = Omit<OmitQueries<OmitMethods<T>>, '
|
28
|
+
export type Selectable<T> = Omit<OmitQueries<OmitMethods<T>>, 'id' | 'xata'>;
|
27
29
|
|
28
30
|
export type Select<T, K extends keyof T> = Pick<T, K> & Queries<T> & XataRecord;
|
29
31
|
|
@@ -43,6 +45,7 @@ type Operator =
|
|
43
45
|
| '$endsWith'
|
44
46
|
| '$startsWith'
|
45
47
|
| '$pattern'
|
48
|
+
| '$is'
|
46
49
|
| '$isNot'
|
47
50
|
| '$contains'
|
48
51
|
| '$includes'
|
@@ -52,7 +55,13 @@ type Operator =
|
|
52
55
|
|
53
56
|
// TODO: restrict constraints depending on type?
|
54
57
|
// E.g. startsWith cannot be used with numbers
|
55
|
-
type Constraint<T> =
|
58
|
+
type Constraint<T> = { [key in Operator]?: T };
|
59
|
+
|
60
|
+
type DeepConstraint<T> = T extends Record<string, any>
|
61
|
+
? {
|
62
|
+
[key in keyof T]?: T[key] | DeepConstraint<T[key]>;
|
63
|
+
}
|
64
|
+
: Constraint<T>;
|
56
65
|
|
57
66
|
type ComparableType = number | Date;
|
58
67
|
|
@@ -67,13 +76,9 @@ export const notExists = (column: string): Constraint<string> => ({ $notExists:
|
|
67
76
|
export const startsWith = (value: string): Constraint<string> => ({ $startsWith: value });
|
68
77
|
export const endsWith = (value: string): Constraint<string> => ({ $endsWith: value });
|
69
78
|
export const pattern = (value: string): Constraint<string> => ({ $pattern: value });
|
79
|
+
export const is = <T>(value: T): Constraint<T> => ({ $is: value });
|
70
80
|
export const isNot = <T>(value: T): Constraint<T> => ({ $isNot: value });
|
71
|
-
export const contains = <T>(value: T): Constraint<T> => {
|
72
|
-
// if (Array.isArray(value)) {
|
73
|
-
// return { $all: value.map(item => ({ $contains: item as string })) }
|
74
|
-
// }
|
75
|
-
return { $contains: value };
|
76
|
-
};
|
81
|
+
export const contains = <T>(value: T): Constraint<T> => ({ $contains: value });
|
77
82
|
|
78
83
|
// TODO: these can only be applied to columns of type "multiple"
|
79
84
|
export const includes = (value: string): Constraint<string> => ({ $includes: value });
|
@@ -82,7 +87,7 @@ export const includesPattern = (value: string): Constraint<string> => ({ $includ
|
|
82
87
|
export const includesAll = (value: string): Constraint<string> => ({ $includesAll: value });
|
83
88
|
|
84
89
|
type FilterConstraints<T> = {
|
85
|
-
[key in keyof T]?: T[key] extends Record<string, any> ? FilterConstraints<T[key]> : T[key] |
|
90
|
+
[key in keyof T]?: T[key] extends Record<string, any> ? FilterConstraints<T[key]> : T[key] | DeepConstraint<T[key]>;
|
86
91
|
};
|
87
92
|
|
88
93
|
type BulkQueryOptions<T> = {
|
@@ -182,7 +187,7 @@ export class Query<T, R = T> {
|
|
182
187
|
}
|
183
188
|
|
184
189
|
filter(constraints: FilterConstraints<T>): Query<T, R>;
|
185
|
-
filter<F extends keyof T>(column: F, value: FilterConstraints<T[F]> |
|
190
|
+
filter<F extends keyof T>(column: F, value: FilterConstraints<T[F]> | DeepConstraint<T[F]>): Query<T, R>;
|
186
191
|
filter(a: any, b?: any): Query<T, R> {
|
187
192
|
if (arguments.length === 1) {
|
188
193
|
const constraints = a as FilterConstraints<T>;
|
@@ -299,13 +304,15 @@ export class RestRepository<T> extends Repository<T> {
|
|
299
304
|
}
|
300
305
|
|
301
306
|
async request(method: string, path: string, body?: unknown) {
|
302
|
-
const { databaseURL } = this.client.options;
|
303
|
-
const
|
307
|
+
const { databaseURL, apiKey } = this.client.options;
|
308
|
+
const branch = await this.client.getBranch();
|
309
|
+
|
310
|
+
const resp: Response = await this.fetch(`${databaseURL}:${branch}${path}`, {
|
304
311
|
method,
|
305
312
|
headers: {
|
306
313
|
Accept: '*/*',
|
307
314
|
'Content-Type': 'application/json',
|
308
|
-
Authorization: `Bearer ${
|
315
|
+
Authorization: `Bearer ${apiKey}`
|
309
316
|
},
|
310
317
|
body: JSON.stringify(body)
|
311
318
|
});
|
@@ -335,8 +342,8 @@ export class RestRepository<T> extends Repository<T> {
|
|
335
342
|
const body = { ...object } as Record<string, unknown>;
|
336
343
|
for (const key of Object.keys(body)) {
|
337
344
|
const value = body[key];
|
338
|
-
if (value && typeof value === 'object' && typeof (value as Record<string, unknown>).
|
339
|
-
body[key] = (value as XataRecord).
|
345
|
+
if (value && typeof value === 'object' && typeof (value as Record<string, unknown>).id === 'string') {
|
346
|
+
body[key] = (value as XataRecord).id;
|
340
347
|
}
|
341
348
|
}
|
342
349
|
const obj = await this.request('POST', `/tables/${this.table}/data`, body);
|
@@ -388,9 +395,15 @@ export class RestRespositoryFactory implements RepositoryFactory {
|
|
388
395
|
}
|
389
396
|
}
|
390
397
|
|
398
|
+
type BranchStrategyValue = string | undefined | null;
|
399
|
+
type BranchStrategyBuilder = () => BranchStrategyValue | Promise<BranchStrategyValue>;
|
400
|
+
type BranchStrategy = BranchStrategyValue | BranchStrategyBuilder;
|
401
|
+
type BranchStrategyOption = NonNullable<BranchStrategy | BranchStrategy[]>;
|
402
|
+
|
391
403
|
export type XataClientOptions = {
|
392
404
|
fetch?: unknown;
|
393
405
|
databaseURL: string;
|
406
|
+
branch: BranchStrategyOption;
|
394
407
|
apiKey: string;
|
395
408
|
repositoryFactory?: RepositoryFactory;
|
396
409
|
};
|
@@ -398,9 +411,14 @@ export type XataClientOptions = {
|
|
398
411
|
export class BaseClient<D extends Record<string, Repository<any>>> {
|
399
412
|
options: XataClientOptions;
|
400
413
|
private links: Links;
|
414
|
+
private branch: BranchStrategyValue;
|
401
415
|
db!: D;
|
402
416
|
|
403
417
|
constructor(options: XataClientOptions, links: Links) {
|
418
|
+
if (!options.databaseURL || !options.apiKey || !options.branch) {
|
419
|
+
throw new Error('Options databaseURL, apiKey and branch are required');
|
420
|
+
}
|
421
|
+
|
404
422
|
this.options = options;
|
405
423
|
this.links = links;
|
406
424
|
}
|
@@ -415,14 +433,14 @@ export class BaseClient<D extends Record<string, Repository<any>>> {
|
|
415
433
|
const value = o[field];
|
416
434
|
|
417
435
|
if (value && typeof value === 'object') {
|
418
|
-
const {
|
419
|
-
if (Object.keys(value).find((col) =>
|
436
|
+
const { id } = value as any;
|
437
|
+
if (Object.keys(value).find((col) => col === 'id')) {
|
420
438
|
o[field] = this.initObject(linkTable, value);
|
421
|
-
} else if (
|
439
|
+
} else if (id) {
|
422
440
|
o[field] = {
|
423
|
-
|
441
|
+
id,
|
424
442
|
get: () => {
|
425
|
-
this.db[linkTable].read(
|
443
|
+
this.db[linkTable].read(id);
|
426
444
|
}
|
427
445
|
};
|
428
446
|
}
|
@@ -431,13 +449,13 @@ export class BaseClient<D extends Record<string, Repository<any>>> {
|
|
431
449
|
|
432
450
|
const db = this.db;
|
433
451
|
o.read = function () {
|
434
|
-
return db[table].read(o['
|
452
|
+
return db[table].read(o['id'] as string);
|
435
453
|
};
|
436
454
|
o.update = function (data: any) {
|
437
|
-
return db[table].update(o['
|
455
|
+
return db[table].update(o['id'] as string, data);
|
438
456
|
};
|
439
457
|
o.delete = function () {
|
440
|
-
return db[table].delete(o['
|
458
|
+
return db[table].delete(o['id'] as string);
|
441
459
|
};
|
442
460
|
|
443
461
|
for (const prop of ['read', 'update', 'delete']) {
|
@@ -449,6 +467,27 @@ export class BaseClient<D extends Record<string, Repository<any>>> {
|
|
449
467
|
Object.freeze(o);
|
450
468
|
return o as T;
|
451
469
|
}
|
470
|
+
|
471
|
+
public async getBranch(): Promise<string> {
|
472
|
+
if (this.branch) return this.branch;
|
473
|
+
|
474
|
+
const { branch: param } = this.options;
|
475
|
+
const strategies = Array.isArray(param) ? [...param] : [param];
|
476
|
+
|
477
|
+
const evaluateBranch = async (strategy: BranchStrategy) => {
|
478
|
+
return isBranchStrategyBuilder(strategy) ? await strategy() : strategy;
|
479
|
+
};
|
480
|
+
|
481
|
+
for await (const strategy of strategies) {
|
482
|
+
const branch = await evaluateBranch(strategy);
|
483
|
+
if (branch) {
|
484
|
+
this.branch = branch;
|
485
|
+
return branch;
|
486
|
+
}
|
487
|
+
}
|
488
|
+
|
489
|
+
throw new Error('Unable to resolve branch value');
|
490
|
+
}
|
452
491
|
}
|
453
492
|
|
454
493
|
export class XataError extends Error {
|
@@ -461,3 +500,7 @@ export class XataError extends Error {
|
|
461
500
|
}
|
462
501
|
|
463
502
|
export type Links = Record<string, Array<string[]>>;
|
503
|
+
|
504
|
+
const isBranchStrategyBuilder = (strategy: BranchStrategy): strategy is BranchStrategyBuilder => {
|
505
|
+
return typeof strategy === 'function';
|
506
|
+
};
|