@xata.io/client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +128 -0
- package/dist/index.js +319 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +182 -0
- package/package.json +22 -0
- package/src/index.test.ts +222 -0
- package/src/index.ts +447 -0
- package/tsconfig.json +21 -0
package/dist/index.d.ts
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
export interface XataRecord {
|
2
|
+
_id: string;
|
3
|
+
_version: number;
|
4
|
+
read(): Promise<this>;
|
5
|
+
update(data: Selectable<this>): Promise<this>;
|
6
|
+
delete(): Promise<void>;
|
7
|
+
}
|
8
|
+
export declare type Queries<T> = {
|
9
|
+
[key in keyof T as T[key] extends Query<infer A, infer B> ? key : never]: T[key];
|
10
|
+
};
|
11
|
+
export declare type OmitQueries<T> = {
|
12
|
+
[key in keyof T as T[key] extends Query<infer A, infer B> ? never : key]: T[key];
|
13
|
+
};
|
14
|
+
export declare type OmitLinks<T> = {
|
15
|
+
[key in keyof T as T[key] extends XataRecord ? never : key]: T[key];
|
16
|
+
};
|
17
|
+
export declare type OmitMethods<T> = {
|
18
|
+
[key in keyof T as T[key] extends Function ? never : key]: T[key];
|
19
|
+
};
|
20
|
+
export declare type Selectable<T> = Omit<OmitQueries<OmitMethods<T>>, '_id' | '_version'>;
|
21
|
+
export declare type Select<T, K extends keyof T> = Pick<T, K> & Queries<T> & XataRecord;
|
22
|
+
export declare type Include<T> = {
|
23
|
+
[key in keyof T as T[key] extends XataRecord ? key : never]?: boolean | Array<keyof Selectable<T[key]>>;
|
24
|
+
};
|
25
|
+
declare type SortDirection = 'asc' | 'desc';
|
26
|
+
declare type Operator = '_gt' | '_lt' | '_gte' | '_lte' | '_exists' | '_notExists' | '_endsWith' | '_startsWith' | '_pattern' | '_isNot' | '_includes' | '_includesSubstring' | '_includesPattern' | '_includesAll';
|
27
|
+
declare type Constraint<T> = Partial<Record<Operator, T>>;
|
28
|
+
declare type ComparableType = number | Date;
|
29
|
+
export declare const gt: <T extends ComparableType>(value: T) => Partial<Record<Operator, T>>;
|
30
|
+
export declare const gte: <T extends ComparableType>(value: T) => Partial<Record<Operator, T>>;
|
31
|
+
export declare const lt: <T extends ComparableType>(value: T) => Partial<Record<Operator, T>>;
|
32
|
+
export declare const lte: <T extends ComparableType>(value: T) => Partial<Record<Operator, T>>;
|
33
|
+
export declare const exists: (column: string) => Constraint<string>;
|
34
|
+
export declare const notExists: (column: string) => Constraint<string>;
|
35
|
+
export declare const startsWith: (value: string) => Constraint<string>;
|
36
|
+
export declare const endsWith: (value: string) => Constraint<string>;
|
37
|
+
export declare const pattern: (value: string) => Constraint<string>;
|
38
|
+
export declare const isNot: <T>(value: T) => Partial<Record<Operator, T>>;
|
39
|
+
export declare const includes: (value: string) => Constraint<string>;
|
40
|
+
export declare const includesSubstring: (value: string) => Constraint<string>;
|
41
|
+
export declare const includesPattern: (value: string) => Constraint<string>;
|
42
|
+
export declare const includesAll: (value: string) => Constraint<string>;
|
43
|
+
declare type FilterConstraints<T> = {
|
44
|
+
[key in keyof T]?: T[key] extends Record<string, any> ? FilterConstraints<T[key]> : T[key] | Constraint<T[key]>;
|
45
|
+
};
|
46
|
+
declare type BulkQueryOptions<T> = {
|
47
|
+
filter?: FilterConstraints<T>;
|
48
|
+
sort?: {
|
49
|
+
column: keyof T;
|
50
|
+
direction?: SortDirection;
|
51
|
+
} | keyof T;
|
52
|
+
};
|
53
|
+
declare type QueryOrConstraint<T, R> = Query<T, R> | Constraint<T>;
|
54
|
+
export declare class Query<T, R = T> {
|
55
|
+
table: string;
|
56
|
+
repository: Repository<T>;
|
57
|
+
readonly _any?: QueryOrConstraint<T, R>[];
|
58
|
+
readonly _all?: QueryOrConstraint<T, R>[];
|
59
|
+
readonly _not?: QueryOrConstraint<T, R>[];
|
60
|
+
readonly _none?: QueryOrConstraint<T, R>[];
|
61
|
+
readonly _sort?: Record<string, SortDirection>;
|
62
|
+
constructor(repository: Repository<T> | null, table: string, data: Partial<Query<T, R>>, parent?: Query<T, R>);
|
63
|
+
any(...queries: Query<T, R>[]): Query<T, R>;
|
64
|
+
all(...queries: Query<T, R>[]): Query<T, R>;
|
65
|
+
not(...queries: Query<T, R>[]): Query<T, R>;
|
66
|
+
none(...queries: Query<T, R>[]): Query<T, R>;
|
67
|
+
filter(constraints: FilterConstraints<T>): Query<T, R>;
|
68
|
+
filter<F extends keyof T>(column: F, value: FilterConstraints<T[F]> | Constraint<T[F]>): Query<T, R>;
|
69
|
+
sort<F extends keyof T>(column: F, direction: SortDirection): Query<T, R>;
|
70
|
+
getMany(options?: BulkQueryOptions<T>): Promise<R[]>;
|
71
|
+
getOne(options?: BulkQueryOptions<T>): Promise<R | null>;
|
72
|
+
deleteAll(): Promise<number>;
|
73
|
+
include(columns: Include<T>): this;
|
74
|
+
toJSON(): {
|
75
|
+
_filter: {
|
76
|
+
_any: QueryOrConstraint<T, R>[] | undefined;
|
77
|
+
_all: QueryOrConstraint<T, R>[] | undefined;
|
78
|
+
_not: QueryOrConstraint<T, R>[] | undefined;
|
79
|
+
_none: QueryOrConstraint<T, R>[] | undefined;
|
80
|
+
} | undefined;
|
81
|
+
_sort: Record<string, SortDirection> | undefined;
|
82
|
+
};
|
83
|
+
}
|
84
|
+
export declare abstract class Repository<T> extends Query<T, Selectable<T>> {
|
85
|
+
select<K extends keyof Selectable<T>>(...columns: K[]): Query<T, Select<T, K>>;
|
86
|
+
abstract create(object: Selectable<T>): Promise<T>;
|
87
|
+
abstract read(id: string): Promise<T | null>;
|
88
|
+
abstract update(id: string, object: Partial<T>): Promise<T>;
|
89
|
+
abstract delete(id: string): void;
|
90
|
+
abstract query<R>(query: Query<T, R>): Promise<R[]>;
|
91
|
+
}
|
92
|
+
export declare class RestRepository<T> extends Repository<T> {
|
93
|
+
client: BaseClient<any>;
|
94
|
+
fetch: any;
|
95
|
+
constructor(client: BaseClient<any>, table: string);
|
96
|
+
request(method: string, path: string, body?: unknown): Promise<any>;
|
97
|
+
select<K extends keyof T>(...columns: K[]): Query<T, Select<T, K>>;
|
98
|
+
create(object: T): Promise<T>;
|
99
|
+
read(id: string): Promise<T | null>;
|
100
|
+
update(id: string, object: Partial<T>): Promise<T>;
|
101
|
+
delete(id: string): Promise<void>;
|
102
|
+
query<R>(query: Query<T, R>): Promise<R[]>;
|
103
|
+
}
|
104
|
+
interface RepositoryFactory {
|
105
|
+
createRepository<T>(client: BaseClient<any>, table: string): Repository<T>;
|
106
|
+
}
|
107
|
+
export declare class RestRespositoryFactory implements RepositoryFactory {
|
108
|
+
createRepository<T>(client: BaseClient<any>, table: string): Repository<T>;
|
109
|
+
}
|
110
|
+
export declare type XataClientOptions = {
|
111
|
+
fetch?: unknown;
|
112
|
+
url: string;
|
113
|
+
token: string;
|
114
|
+
repositoryFactory?: RepositoryFactory;
|
115
|
+
};
|
116
|
+
export declare class BaseClient<D extends Record<string, Repository<any>>> {
|
117
|
+
options: XataClientOptions;
|
118
|
+
private links;
|
119
|
+
db: D;
|
120
|
+
constructor(options: XataClientOptions, links: Links);
|
121
|
+
initObject<T>(table: string, object: object): T;
|
122
|
+
}
|
123
|
+
export declare class XataError extends Error {
|
124
|
+
readonly status: number;
|
125
|
+
constructor(message: string, status: number);
|
126
|
+
}
|
127
|
+
export declare type Links = Record<string, Array<string[]>>;
|
128
|
+
export {};
|
package/dist/index.js
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
|
+
});
|
10
|
+
};
|
11
|
+
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.isNot = exports.pattern = exports.endsWith = exports.startsWith = exports.notExists = exports.exists = exports.lte = exports.lt = exports.gte = exports.gt = void 0;
|
13
|
+
const gt = (value) => ({ _gt: value });
|
14
|
+
exports.gt = gt;
|
15
|
+
const gte = (value) => ({ _gte: value });
|
16
|
+
exports.gte = gte;
|
17
|
+
const lt = (value) => ({ _lt: value });
|
18
|
+
exports.lt = lt;
|
19
|
+
const lte = (value) => ({ _lte: value });
|
20
|
+
exports.lte = lte;
|
21
|
+
const exists = (column) => ({ _exists: column });
|
22
|
+
exports.exists = exists;
|
23
|
+
const notExists = (column) => ({ _notExists: column });
|
24
|
+
exports.notExists = notExists;
|
25
|
+
const startsWith = (value) => ({ _startsWith: value });
|
26
|
+
exports.startsWith = startsWith;
|
27
|
+
const endsWith = (value) => ({ _endsWith: value });
|
28
|
+
exports.endsWith = endsWith;
|
29
|
+
const pattern = (value) => ({ _pattern: value });
|
30
|
+
exports.pattern = pattern;
|
31
|
+
const isNot = (value) => ({ _isNot: value });
|
32
|
+
exports.isNot = isNot;
|
33
|
+
// TODO: these can only be applied to columns of type "multiple"
|
34
|
+
const includes = (value) => ({ _includes: value });
|
35
|
+
exports.includes = includes;
|
36
|
+
const includesSubstring = (value) => ({ _includesSubstring: value });
|
37
|
+
exports.includesSubstring = includesSubstring;
|
38
|
+
const includesPattern = (value) => ({ _includesPattern: value });
|
39
|
+
exports.includesPattern = includesPattern;
|
40
|
+
const includesAll = (value) => ({ _includesAll: value });
|
41
|
+
exports.includesAll = includesAll;
|
42
|
+
class Query {
|
43
|
+
constructor(repository, table, data, parent) {
|
44
|
+
if (repository) {
|
45
|
+
this.repository = repository;
|
46
|
+
}
|
47
|
+
else {
|
48
|
+
this.repository = this;
|
49
|
+
}
|
50
|
+
this.table = table;
|
51
|
+
// For some reason Object.assign(this, parent) didn't work in this case
|
52
|
+
// so doing all this manually:
|
53
|
+
this._any = parent === null || parent === void 0 ? void 0 : parent._any;
|
54
|
+
this._all = parent === null || parent === void 0 ? void 0 : parent._all;
|
55
|
+
this._not = parent === null || parent === void 0 ? void 0 : parent._not;
|
56
|
+
this._none = parent === null || parent === void 0 ? void 0 : parent._none;
|
57
|
+
this._sort = parent === null || parent === void 0 ? void 0 : parent._sort;
|
58
|
+
Object.assign(this, data);
|
59
|
+
// These bindings are used to support deconstructing
|
60
|
+
// const { any, not, filter, sort } = xata.users.query()
|
61
|
+
this.any = this.any.bind(this);
|
62
|
+
this.all = this.all.bind(this);
|
63
|
+
this.not = this.not.bind(this);
|
64
|
+
this.filter = this.filter.bind(this);
|
65
|
+
this.sort = this.sort.bind(this);
|
66
|
+
this.none = this.none.bind(this);
|
67
|
+
}
|
68
|
+
any(...queries) {
|
69
|
+
return new Query(this.repository, this.table, {
|
70
|
+
_any: (this._any || []).concat(queries)
|
71
|
+
}, this);
|
72
|
+
}
|
73
|
+
all(...queries) {
|
74
|
+
return new Query(this.repository, this.table, {
|
75
|
+
_all: (this._all || []).concat(queries)
|
76
|
+
}, this);
|
77
|
+
}
|
78
|
+
not(...queries) {
|
79
|
+
return new Query(this.repository, this.table, {
|
80
|
+
_not: (this._not || []).concat(queries)
|
81
|
+
}, this);
|
82
|
+
}
|
83
|
+
none(...queries) {
|
84
|
+
return new Query(this.repository, this.table, {
|
85
|
+
_none: (this._none || []).concat(queries)
|
86
|
+
}, this);
|
87
|
+
}
|
88
|
+
filter(a, b) {
|
89
|
+
if (arguments.length === 1) {
|
90
|
+
const constraints = a;
|
91
|
+
const queries = [];
|
92
|
+
for (const [column, constraint] of Object.entries(constraints)) {
|
93
|
+
queries.push({ [column]: constraint });
|
94
|
+
}
|
95
|
+
return new Query(this.repository, this.table, {
|
96
|
+
_any: (this._any || []).concat(queries)
|
97
|
+
}, this);
|
98
|
+
}
|
99
|
+
else {
|
100
|
+
const column = a;
|
101
|
+
const value = b;
|
102
|
+
return new Query(this.repository, this.table, {
|
103
|
+
_any: (this._any || []).concat({ [column]: value })
|
104
|
+
}, this);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
sort(column, direction) {
|
108
|
+
const sort = Object.assign(Object.assign({}, this._sort), { [column]: direction });
|
109
|
+
const q = new Query(this.repository, this.table, {
|
110
|
+
_sort: sort
|
111
|
+
}, this);
|
112
|
+
return q;
|
113
|
+
}
|
114
|
+
// TODO: pagination. Maybe implement different methods for different type of paginations
|
115
|
+
// and one to simply get the first records returned by the query with no pagination.
|
116
|
+
getMany(options) {
|
117
|
+
return __awaiter(this, void 0, void 0, function* () {
|
118
|
+
// TODO: use options
|
119
|
+
return this.repository.query(this);
|
120
|
+
});
|
121
|
+
}
|
122
|
+
getOne(options) {
|
123
|
+
return __awaiter(this, void 0, void 0, function* () {
|
124
|
+
// TODO: use options
|
125
|
+
const arr = yield this.getMany(); // TODO, limit to 1
|
126
|
+
return arr[0] || null;
|
127
|
+
});
|
128
|
+
}
|
129
|
+
deleteAll() {
|
130
|
+
return __awaiter(this, void 0, void 0, function* () {
|
131
|
+
// Return number of affected rows
|
132
|
+
return 0;
|
133
|
+
});
|
134
|
+
}
|
135
|
+
include(columns) {
|
136
|
+
// TODO
|
137
|
+
return this;
|
138
|
+
}
|
139
|
+
toJSON() {
|
140
|
+
const _filter = {
|
141
|
+
_any: this._any,
|
142
|
+
_all: this._all,
|
143
|
+
_not: this._not,
|
144
|
+
_none: this._none
|
145
|
+
};
|
146
|
+
return {
|
147
|
+
_filter: Object.values(_filter).some(Boolean) ? _filter : undefined,
|
148
|
+
_sort: this._sort
|
149
|
+
};
|
150
|
+
}
|
151
|
+
}
|
152
|
+
exports.Query = Query;
|
153
|
+
class Repository extends Query {
|
154
|
+
select(...columns) {
|
155
|
+
return new Query(this.repository, this.table, {});
|
156
|
+
}
|
157
|
+
}
|
158
|
+
exports.Repository = Repository;
|
159
|
+
class RestRepository extends Repository {
|
160
|
+
constructor(client, table) {
|
161
|
+
super(null, table, {});
|
162
|
+
this.client = client;
|
163
|
+
const { fetch } = client.options;
|
164
|
+
if (fetch) {
|
165
|
+
this.fetch = fetch;
|
166
|
+
}
|
167
|
+
else if (typeof window === 'object') {
|
168
|
+
this.fetch = window.fetch;
|
169
|
+
}
|
170
|
+
else if (typeof require === 'function') {
|
171
|
+
try {
|
172
|
+
this.fetch = require('node-fetch');
|
173
|
+
}
|
174
|
+
catch (err) {
|
175
|
+
try {
|
176
|
+
this.fetch = require('cross-fetch');
|
177
|
+
}
|
178
|
+
catch (err) {
|
179
|
+
throw new Error('No fetch implementation found. Please provide one in the constructor');
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
Object.defineProperty(this, 'client', { enumerable: false });
|
184
|
+
Object.defineProperty(this, 'fetch', { enumerable: false });
|
185
|
+
Object.defineProperty(this, 'hostname', { enumerable: false });
|
186
|
+
}
|
187
|
+
request(method, path, body) {
|
188
|
+
return __awaiter(this, void 0, void 0, function* () {
|
189
|
+
const { url: xatabaseURL } = this.client.options;
|
190
|
+
const resp = yield this.fetch(`${xatabaseURL}${path}`, {
|
191
|
+
method,
|
192
|
+
headers: {
|
193
|
+
Accept: '*/*',
|
194
|
+
'Content-Type': 'application/json',
|
195
|
+
Authorization: `Bearer ${this.client.options.token}`
|
196
|
+
},
|
197
|
+
body: JSON.stringify(body)
|
198
|
+
});
|
199
|
+
if (!resp.ok) {
|
200
|
+
try {
|
201
|
+
const json = yield resp.json();
|
202
|
+
const message = json.message;
|
203
|
+
if (typeof message === 'string') {
|
204
|
+
throw new XataError(message, resp.status);
|
205
|
+
}
|
206
|
+
}
|
207
|
+
catch (err) {
|
208
|
+
if (err instanceof XataError)
|
209
|
+
throw err;
|
210
|
+
// Ignore errors for other reasons.
|
211
|
+
// For example if the response's body cannot be parsed as JSON
|
212
|
+
}
|
213
|
+
throw new XataError(resp.statusText, resp.status);
|
214
|
+
}
|
215
|
+
if (resp.status === 204)
|
216
|
+
return;
|
217
|
+
return resp.json();
|
218
|
+
});
|
219
|
+
}
|
220
|
+
select(...columns) {
|
221
|
+
return new Query(this.repository, this.table, {});
|
222
|
+
}
|
223
|
+
create(object) {
|
224
|
+
return __awaiter(this, void 0, void 0, function* () {
|
225
|
+
const obj = yield this.request('POST', `/tables/${this.table}/data`, object);
|
226
|
+
return this.client.initObject(this.table, obj);
|
227
|
+
});
|
228
|
+
}
|
229
|
+
read(id) {
|
230
|
+
return __awaiter(this, void 0, void 0, function* () {
|
231
|
+
try {
|
232
|
+
const obj = yield this.request('GET', `/tables/${this.table}/data/${id}`);
|
233
|
+
return this.client.initObject(this.table, obj);
|
234
|
+
}
|
235
|
+
catch (err) {
|
236
|
+
if (err.status === 404)
|
237
|
+
return null;
|
238
|
+
throw err;
|
239
|
+
}
|
240
|
+
});
|
241
|
+
}
|
242
|
+
update(id, object) {
|
243
|
+
return __awaiter(this, void 0, void 0, function* () {
|
244
|
+
const obj = yield this.request('PUT', `/tables/${this.table}/data/${id}`, object);
|
245
|
+
return this.client.initObject(this.table, obj);
|
246
|
+
});
|
247
|
+
}
|
248
|
+
delete(id) {
|
249
|
+
return __awaiter(this, void 0, void 0, function* () {
|
250
|
+
yield this.request('DELETE', `/tables/${this.table}/data/${id}`);
|
251
|
+
});
|
252
|
+
}
|
253
|
+
query(query) {
|
254
|
+
return __awaiter(this, void 0, void 0, function* () {
|
255
|
+
const result = yield this.request('POST', `/tables/${this.table}/query`, query);
|
256
|
+
return result.records.map((record) => this.client.initObject(this.table, record));
|
257
|
+
});
|
258
|
+
}
|
259
|
+
}
|
260
|
+
exports.RestRepository = RestRepository;
|
261
|
+
class RestRespositoryFactory {
|
262
|
+
createRepository(client, table) {
|
263
|
+
return new RestRepository(client, table);
|
264
|
+
}
|
265
|
+
}
|
266
|
+
exports.RestRespositoryFactory = RestRespositoryFactory;
|
267
|
+
class BaseClient {
|
268
|
+
constructor(options, links) {
|
269
|
+
this.options = options;
|
270
|
+
this.links = links;
|
271
|
+
}
|
272
|
+
initObject(table, object) {
|
273
|
+
const o = {};
|
274
|
+
Object.assign(o, object);
|
275
|
+
const tableLinks = this.links[table] || [];
|
276
|
+
for (const link of tableLinks) {
|
277
|
+
const [field, linkTable] = link;
|
278
|
+
const value = o[field];
|
279
|
+
if (value && typeof value === 'object') {
|
280
|
+
const { _id } = value;
|
281
|
+
if (Object.keys(value).find((col) => !col.startsWith('_'))) {
|
282
|
+
o[field] = this.initObject(linkTable, value);
|
283
|
+
}
|
284
|
+
else if (_id) {
|
285
|
+
o[field] = {
|
286
|
+
_id,
|
287
|
+
get: () => {
|
288
|
+
this.db[linkTable].read(_id);
|
289
|
+
}
|
290
|
+
};
|
291
|
+
}
|
292
|
+
}
|
293
|
+
}
|
294
|
+
const db = this.db;
|
295
|
+
o.read = function () {
|
296
|
+
return db[table].read(o['_id']);
|
297
|
+
};
|
298
|
+
o.update = function (data) {
|
299
|
+
return db[table].update(o['_id'], data);
|
300
|
+
};
|
301
|
+
o.delete = function () {
|
302
|
+
return db[table].delete(o['_id']);
|
303
|
+
};
|
304
|
+
for (const prop of ['read', 'update', 'delete']) {
|
305
|
+
Object.defineProperty(o, prop, { enumerable: false });
|
306
|
+
}
|
307
|
+
// TODO: links and rev links
|
308
|
+
Object.freeze(o);
|
309
|
+
return o;
|
310
|
+
}
|
311
|
+
}
|
312
|
+
exports.BaseClient = BaseClient;
|
313
|
+
class XataError extends Error {
|
314
|
+
constructor(message, status) {
|
315
|
+
super(message);
|
316
|
+
this.status = status;
|
317
|
+
}
|
318
|
+
}
|
319
|
+
exports.XataError = XataError;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,182 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
|
+
});
|
10
|
+
};
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
12
|
+
const _1 = require("./");
|
13
|
+
const fetch = jest.fn();
|
14
|
+
const client = new _1.BaseClient({
|
15
|
+
fetch,
|
16
|
+
token: '1234',
|
17
|
+
url: 'https://my-workspace-5df34do.staging.xatabase.co/db/xata:main'
|
18
|
+
}, {});
|
19
|
+
const users = new _1.RestRepository(client, 'users');
|
20
|
+
describe('request', () => {
|
21
|
+
test('builds the right arguments for a GET request', () => __awaiter(void 0, void 0, void 0, function* () {
|
22
|
+
fetch.mockReset().mockImplementation(() => {
|
23
|
+
return {
|
24
|
+
ok: true,
|
25
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
|
26
|
+
};
|
27
|
+
});
|
28
|
+
users.request('GET', '/foo');
|
29
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
30
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
31
|
+
Array [
|
32
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/foo",
|
33
|
+
Object {
|
34
|
+
"body": undefined,
|
35
|
+
"headers": Object {
|
36
|
+
"Accept": "*/*",
|
37
|
+
"Authorization": "Bearer 1234",
|
38
|
+
"Content-Type": "application/json",
|
39
|
+
},
|
40
|
+
"method": "GET",
|
41
|
+
},
|
42
|
+
]
|
43
|
+
`);
|
44
|
+
}));
|
45
|
+
test('builds the right arguments for a POST request', () => __awaiter(void 0, void 0, void 0, function* () {
|
46
|
+
fetch.mockReset().mockImplementation(() => {
|
47
|
+
return {
|
48
|
+
ok: true,
|
49
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
|
50
|
+
};
|
51
|
+
});
|
52
|
+
users.request('POST', '/foo', { a: 1 });
|
53
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
54
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
55
|
+
Array [
|
56
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/foo",
|
57
|
+
Object {
|
58
|
+
"body": "{\\"a\\":1}",
|
59
|
+
"headers": Object {
|
60
|
+
"Accept": "*/*",
|
61
|
+
"Authorization": "Bearer 1234",
|
62
|
+
"Content-Type": "application/json",
|
63
|
+
},
|
64
|
+
"method": "POST",
|
65
|
+
},
|
66
|
+
]
|
67
|
+
`);
|
68
|
+
}));
|
69
|
+
test('throws if the response is not ok', () => __awaiter(void 0, void 0, void 0, function* () {
|
70
|
+
fetch.mockImplementation(() => {
|
71
|
+
return {
|
72
|
+
ok: false,
|
73
|
+
status: 404,
|
74
|
+
statusText: 'Not Found'
|
75
|
+
};
|
76
|
+
});
|
77
|
+
expect(users.request('GET', '/foo')).rejects.toThrow(new _1.XataError('Not Found', 404));
|
78
|
+
}));
|
79
|
+
test('throws with the error from the server if the response is not ok', () => __awaiter(void 0, void 0, void 0, function* () {
|
80
|
+
fetch.mockImplementation(() => {
|
81
|
+
return {
|
82
|
+
ok: false,
|
83
|
+
status: 404,
|
84
|
+
statusText: 'Not Found',
|
85
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({ message: 'Resource not found' }); })
|
86
|
+
};
|
87
|
+
});
|
88
|
+
expect(users.request('GET', '/foo')).rejects.toThrow(new _1.XataError('Resource not found', 404));
|
89
|
+
}));
|
90
|
+
test('returns the json body if the response is ok', () => __awaiter(void 0, void 0, void 0, function* () {
|
91
|
+
const json = { a: 1 };
|
92
|
+
fetch.mockImplementation(() => {
|
93
|
+
return {
|
94
|
+
ok: true,
|
95
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return json; })
|
96
|
+
};
|
97
|
+
});
|
98
|
+
const result = yield users.request('GET', '/foo');
|
99
|
+
expect(result).toEqual(json);
|
100
|
+
}));
|
101
|
+
});
|
102
|
+
function expectRequest(expectedRequest, callback, response) {
|
103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
104
|
+
const request = jest.fn(() => __awaiter(this, void 0, void 0, function* () { return response; }));
|
105
|
+
users.request = request;
|
106
|
+
yield callback();
|
107
|
+
const { calls } = request.mock;
|
108
|
+
expect(calls.length).toBe(1);
|
109
|
+
const [method, path, body] = calls[0];
|
110
|
+
expect(method).toBe(expectedRequest.method);
|
111
|
+
expect(path).toBe(expectedRequest.path);
|
112
|
+
expect(JSON.stringify(body)).toBe(JSON.stringify(expectedRequest.body));
|
113
|
+
});
|
114
|
+
}
|
115
|
+
describe('query', () => {
|
116
|
+
describe('getMany', () => {
|
117
|
+
test('simple query', () => __awaiter(void 0, void 0, void 0, function* () {
|
118
|
+
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
119
|
+
expectRequest(expected, () => users.getMany(), { records: [] });
|
120
|
+
}));
|
121
|
+
test('query with one filter', () => __awaiter(void 0, void 0, void 0, function* () {
|
122
|
+
const expected = { method: 'POST', path: '/tables/users/query', body: { _filter: { _any: [{ name: 'foo' }] } } };
|
123
|
+
expectRequest(expected, () => users.filter('name', 'foo').getMany(), { records: [] });
|
124
|
+
}));
|
125
|
+
});
|
126
|
+
describe('getOne', () => {
|
127
|
+
test('returns a single object', () => __awaiter(void 0, void 0, void 0, function* () {
|
128
|
+
const result = { records: [{ _id: '1234' }] };
|
129
|
+
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
130
|
+
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
131
|
+
const first = yield users.select().getOne();
|
132
|
+
expect(first === null || first === void 0 ? void 0 : first._id).toBe(result.records[0]._id);
|
133
|
+
}), result);
|
134
|
+
}));
|
135
|
+
test('returns null if no objects are returned', () => __awaiter(void 0, void 0, void 0, function* () {
|
136
|
+
const result = { records: [] };
|
137
|
+
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
138
|
+
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
139
|
+
const first = yield users.getOne();
|
140
|
+
expect(first).toBeNull();
|
141
|
+
}), result);
|
142
|
+
}));
|
143
|
+
});
|
144
|
+
});
|
145
|
+
describe('read', () => {
|
146
|
+
test('reads an object by id successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
147
|
+
const id = 'rec_1234';
|
148
|
+
const expected = { method: 'GET', path: `/tables/users/data/${id}`, body: undefined };
|
149
|
+
expectRequest(expected, () => users.read(id));
|
150
|
+
}));
|
151
|
+
});
|
152
|
+
describe('Repository.update', () => {
|
153
|
+
test('updates and object successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
154
|
+
const object = { _id: 'rec_1234', _version: 1, name: 'Ada' };
|
155
|
+
const expected = { method: 'PUT', path: `/tables/users/data/${object._id}`, body: object };
|
156
|
+
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
157
|
+
const result = yield users.update(object._id, object);
|
158
|
+
expect(result._id).toBe(object._id);
|
159
|
+
}), { _id: object._id });
|
160
|
+
}));
|
161
|
+
});
|
162
|
+
describe('Repository.delete', () => {
|
163
|
+
test('deletes a record by id successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
164
|
+
const id = 'rec_1234';
|
165
|
+
const expected = { method: 'DELETE', path: `/tables/users/data/${id}`, body: undefined };
|
166
|
+
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
167
|
+
const result = yield users.delete(id);
|
168
|
+
expect(result).toBe(undefined);
|
169
|
+
}));
|
170
|
+
}));
|
171
|
+
});
|
172
|
+
describe('create', () => {
|
173
|
+
test('successful', () => __awaiter(void 0, void 0, void 0, function* () {
|
174
|
+
const created = { _id: 'rec_1234', _version: 0 };
|
175
|
+
const object = { name: 'Ada' };
|
176
|
+
const expected = { method: 'POST', path: '/tables/users/data', body: object };
|
177
|
+
expectRequest(expected, () => __awaiter(void 0, void 0, void 0, function* () {
|
178
|
+
const result = yield users.create(object);
|
179
|
+
expect(result._id).toBe(created._id);
|
180
|
+
}), created);
|
181
|
+
}));
|
182
|
+
});
|
package/package.json
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"name": "@xata.io/client",
|
3
|
+
"version": "0.1.0",
|
4
|
+
"description": "Xata.io SDK for TypesScript and JavaScript",
|
5
|
+
"main": "index.js",
|
6
|
+
"scripts": {
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
8
|
+
"build": "tsc",
|
9
|
+
"prepack": "npm run build"
|
10
|
+
},
|
11
|
+
"repository": {
|
12
|
+
"type": "git",
|
13
|
+
"url": "git+https://github.com/xataio/client-ts.git"
|
14
|
+
},
|
15
|
+
"keywords": [],
|
16
|
+
"author": "",
|
17
|
+
"license": "MIT",
|
18
|
+
"bugs": {
|
19
|
+
"url": "https://github.com/xataio/client-ts/issues"
|
20
|
+
},
|
21
|
+
"homepage": "https://github.com/xataio/client-ts#readme"
|
22
|
+
}
|
@@ -0,0 +1,222 @@
|
|
1
|
+
import { BaseClient, RestRepository, XataError, XataRecord } from './';
|
2
|
+
|
3
|
+
const fetch = jest.fn();
|
4
|
+
const client = new BaseClient(
|
5
|
+
{
|
6
|
+
fetch,
|
7
|
+
token: '1234',
|
8
|
+
url: 'https://my-workspace-5df34do.staging.xatabase.co/db/xata:main'
|
9
|
+
},
|
10
|
+
{}
|
11
|
+
);
|
12
|
+
|
13
|
+
interface User extends XataRecord {
|
14
|
+
name: string;
|
15
|
+
}
|
16
|
+
|
17
|
+
const users = new RestRepository<User>(client, 'users');
|
18
|
+
|
19
|
+
describe('request', () => {
|
20
|
+
test('builds the right arguments for a GET request', async () => {
|
21
|
+
fetch.mockReset().mockImplementation(() => {
|
22
|
+
return {
|
23
|
+
ok: true,
|
24
|
+
json: async () => ({})
|
25
|
+
};
|
26
|
+
});
|
27
|
+
|
28
|
+
users.request('GET', '/foo');
|
29
|
+
|
30
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
31
|
+
expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
|
32
|
+
Array [
|
33
|
+
"https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/foo",
|
34
|
+
Object {
|
35
|
+
"body": undefined,
|
36
|
+
"headers": Object {
|
37
|
+
"Accept": "*/*",
|
38
|
+
"Authorization": "Bearer 1234",
|
39
|
+
"Content-Type": "application/json",
|
40
|
+
},
|
41
|
+
"method": "GET",
|
42
|
+
},
|
43
|
+
]
|
44
|
+
`);
|
45
|
+
});
|
46
|
+
|
47
|
+
test('builds the right arguments for a POST request', async () => {
|
48
|
+
fetch.mockReset().mockImplementation(() => {
|
49
|
+
return {
|
50
|
+
ok: true,
|
51
|
+
json: async () => ({})
|
52
|
+
};
|
53
|
+
});
|
54
|
+
|
55
|
+
users.request('POST', '/foo', { a: 1 });
|
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:main/foo",
|
61
|
+
Object {
|
62
|
+
"body": "{\\"a\\":1}",
|
63
|
+
"headers": Object {
|
64
|
+
"Accept": "*/*",
|
65
|
+
"Authorization": "Bearer 1234",
|
66
|
+
"Content-Type": "application/json",
|
67
|
+
},
|
68
|
+
"method": "POST",
|
69
|
+
},
|
70
|
+
]
|
71
|
+
`);
|
72
|
+
});
|
73
|
+
|
74
|
+
test('throws if the response is not ok', async () => {
|
75
|
+
fetch.mockImplementation(() => {
|
76
|
+
return {
|
77
|
+
ok: false,
|
78
|
+
status: 404,
|
79
|
+
statusText: 'Not Found'
|
80
|
+
};
|
81
|
+
});
|
82
|
+
|
83
|
+
expect(users.request('GET', '/foo')).rejects.toThrow(new XataError('Not Found', 404));
|
84
|
+
});
|
85
|
+
|
86
|
+
test('throws with the error from the server if the response is not ok', async () => {
|
87
|
+
fetch.mockImplementation(() => {
|
88
|
+
return {
|
89
|
+
ok: false,
|
90
|
+
status: 404,
|
91
|
+
statusText: 'Not Found',
|
92
|
+
json: async () => ({ message: 'Resource not found' })
|
93
|
+
};
|
94
|
+
});
|
95
|
+
|
96
|
+
expect(users.request('GET', '/foo')).rejects.toThrow(new XataError('Resource not found', 404));
|
97
|
+
});
|
98
|
+
|
99
|
+
test('returns the json body if the response is ok', async () => {
|
100
|
+
const json = { a: 1 };
|
101
|
+
fetch.mockImplementation(() => {
|
102
|
+
return {
|
103
|
+
ok: true,
|
104
|
+
json: async () => json
|
105
|
+
};
|
106
|
+
});
|
107
|
+
|
108
|
+
const result = await users.request('GET', '/foo');
|
109
|
+
expect(result).toEqual(json);
|
110
|
+
});
|
111
|
+
});
|
112
|
+
|
113
|
+
type ExpectedRequest = {
|
114
|
+
method: string;
|
115
|
+
path: string;
|
116
|
+
body: unknown;
|
117
|
+
};
|
118
|
+
|
119
|
+
async function expectRequest(expectedRequest: ExpectedRequest, callback: () => void, response?: unknown) {
|
120
|
+
const request = jest.fn(async () => response);
|
121
|
+
users.request = request;
|
122
|
+
|
123
|
+
await callback();
|
124
|
+
|
125
|
+
const { calls } = request.mock;
|
126
|
+
expect(calls.length).toBe(1);
|
127
|
+
const [method, path, body] = calls[0] as any;
|
128
|
+
expect(method).toBe(expectedRequest.method);
|
129
|
+
expect(path).toBe(expectedRequest.path);
|
130
|
+
expect(JSON.stringify(body)).toBe(JSON.stringify(expectedRequest.body));
|
131
|
+
}
|
132
|
+
|
133
|
+
describe('query', () => {
|
134
|
+
describe('getMany', () => {
|
135
|
+
test('simple query', async () => {
|
136
|
+
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
137
|
+
expectRequest(expected, () => users.getMany(), { records: [] });
|
138
|
+
});
|
139
|
+
|
140
|
+
test('query with one filter', async () => {
|
141
|
+
const expected = { method: 'POST', path: '/tables/users/query', body: { _filter: { _any: [{ name: 'foo' }] } } };
|
142
|
+
expectRequest(expected, () => users.filter('name', 'foo').getMany(), { records: [] });
|
143
|
+
});
|
144
|
+
});
|
145
|
+
|
146
|
+
describe('getOne', () => {
|
147
|
+
test('returns a single object', async () => {
|
148
|
+
const result = { records: [{ _id: '1234' }] };
|
149
|
+
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
150
|
+
expectRequest(
|
151
|
+
expected,
|
152
|
+
async () => {
|
153
|
+
const first = await users.select().getOne();
|
154
|
+
expect(first?._id).toBe(result.records[0]._id);
|
155
|
+
},
|
156
|
+
result
|
157
|
+
);
|
158
|
+
});
|
159
|
+
|
160
|
+
test('returns null if no objects are returned', async () => {
|
161
|
+
const result = { records: [] };
|
162
|
+
const expected = { method: 'POST', path: '/tables/users/query', body: {} };
|
163
|
+
expectRequest(
|
164
|
+
expected,
|
165
|
+
async () => {
|
166
|
+
const first = await users.getOne();
|
167
|
+
expect(first).toBeNull();
|
168
|
+
},
|
169
|
+
result
|
170
|
+
);
|
171
|
+
});
|
172
|
+
});
|
173
|
+
});
|
174
|
+
|
175
|
+
describe('read', () => {
|
176
|
+
test('reads an object by id successfully', async () => {
|
177
|
+
const id = 'rec_1234';
|
178
|
+
const expected = { method: 'GET', path: `/tables/users/data/${id}`, body: undefined };
|
179
|
+
expectRequest(expected, () => users.read(id));
|
180
|
+
});
|
181
|
+
});
|
182
|
+
|
183
|
+
describe('Repository.update', () => {
|
184
|
+
test('updates and object successfully', async () => {
|
185
|
+
const object = { _id: 'rec_1234', _version: 1, name: 'Ada' } as User;
|
186
|
+
const expected = { method: 'PUT', path: `/tables/users/data/${object._id}`, body: object };
|
187
|
+
expectRequest(
|
188
|
+
expected,
|
189
|
+
async () => {
|
190
|
+
const result = await users.update(object._id, object);
|
191
|
+
expect(result._id).toBe(object._id);
|
192
|
+
},
|
193
|
+
{ _id: object._id }
|
194
|
+
);
|
195
|
+
});
|
196
|
+
});
|
197
|
+
describe('Repository.delete', () => {
|
198
|
+
test('deletes a record by id successfully', async () => {
|
199
|
+
const id = 'rec_1234';
|
200
|
+
const expected = { method: 'DELETE', path: `/tables/users/data/${id}`, body: undefined };
|
201
|
+
expectRequest(expected, async () => {
|
202
|
+
const result = await users.delete(id);
|
203
|
+
expect(result).toBe(undefined);
|
204
|
+
});
|
205
|
+
});
|
206
|
+
});
|
207
|
+
|
208
|
+
describe('create', () => {
|
209
|
+
test('successful', async () => {
|
210
|
+
const created = { _id: 'rec_1234', _version: 0 };
|
211
|
+
const object = { name: 'Ada' } as User;
|
212
|
+
const expected = { method: 'POST', path: '/tables/users/data', body: object };
|
213
|
+
expectRequest(
|
214
|
+
expected,
|
215
|
+
async () => {
|
216
|
+
const result = await users.create(object);
|
217
|
+
expect(result._id).toBe(created._id);
|
218
|
+
},
|
219
|
+
created
|
220
|
+
);
|
221
|
+
});
|
222
|
+
});
|
package/src/index.ts
ADDED
@@ -0,0 +1,447 @@
|
|
1
|
+
export interface XataRecord {
|
2
|
+
_id: string;
|
3
|
+
_version: number;
|
4
|
+
read(): Promise<this>;
|
5
|
+
update(data: Selectable<this>): Promise<this>;
|
6
|
+
delete(): Promise<void>;
|
7
|
+
}
|
8
|
+
|
9
|
+
export type Queries<T> = {
|
10
|
+
[key in keyof T as T[key] extends Query<infer A, infer B> ? key : never]: T[key];
|
11
|
+
};
|
12
|
+
|
13
|
+
export type OmitQueries<T> = {
|
14
|
+
[key in keyof T as T[key] extends Query<infer A, infer B> ? never : key]: T[key];
|
15
|
+
};
|
16
|
+
|
17
|
+
export type OmitLinks<T> = {
|
18
|
+
[key in keyof T as T[key] extends XataRecord ? never : key]: T[key];
|
19
|
+
};
|
20
|
+
|
21
|
+
export type OmitMethods<T> = {
|
22
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
23
|
+
[key in keyof T as T[key] extends Function ? never : key]: T[key];
|
24
|
+
};
|
25
|
+
|
26
|
+
export type Selectable<T> = Omit<OmitQueries<OmitMethods<T>>, '_id' | '_version'>;
|
27
|
+
|
28
|
+
export type Select<T, K extends keyof T> = Pick<T, K> & Queries<T> & XataRecord;
|
29
|
+
|
30
|
+
export type Include<T> = {
|
31
|
+
[key in keyof T as T[key] extends XataRecord ? key : never]?: boolean | Array<keyof Selectable<T[key]>>;
|
32
|
+
};
|
33
|
+
|
34
|
+
type SortDirection = 'asc' | 'desc';
|
35
|
+
|
36
|
+
type Operator =
|
37
|
+
| '_gt'
|
38
|
+
| '_lt'
|
39
|
+
| '_gte'
|
40
|
+
| '_lte'
|
41
|
+
| '_exists'
|
42
|
+
| '_notExists'
|
43
|
+
| '_endsWith'
|
44
|
+
| '_startsWith'
|
45
|
+
| '_pattern'
|
46
|
+
| '_isNot'
|
47
|
+
| '_includes'
|
48
|
+
| '_includesSubstring'
|
49
|
+
| '_includesPattern'
|
50
|
+
| '_includesAll';
|
51
|
+
|
52
|
+
// TODO: restrict constraints depending on type?
|
53
|
+
// E.g. startsWith cannot be used with numbers
|
54
|
+
type Constraint<T> = Partial<Record<Operator, T>>;
|
55
|
+
|
56
|
+
type ComparableType = number | Date;
|
57
|
+
|
58
|
+
export const gt = <T extends ComparableType>(value: T): Constraint<T> => ({ _gt: value });
|
59
|
+
export const gte = <T extends ComparableType>(value: T): Constraint<T> => ({ _gte: value });
|
60
|
+
export const lt = <T extends ComparableType>(value: T): Constraint<T> => ({ _lt: value });
|
61
|
+
export const lte = <T extends ComparableType>(value: T): Constraint<T> => ({ _lte: value });
|
62
|
+
export const exists = (column: string): Constraint<string> => ({ _exists: column });
|
63
|
+
export const notExists = (column: string): Constraint<string> => ({ _notExists: column });
|
64
|
+
export const startsWith = (value: string): Constraint<string> => ({ _startsWith: value });
|
65
|
+
export const endsWith = (value: string): Constraint<string> => ({ _endsWith: value });
|
66
|
+
export const pattern = (value: string): Constraint<string> => ({ _pattern: value });
|
67
|
+
export const isNot = <T>(value: T): Constraint<T> => ({ _isNot: value });
|
68
|
+
|
69
|
+
// TODO: these can only be applied to columns of type "multiple"
|
70
|
+
export const includes = (value: string): Constraint<string> => ({ _includes: value });
|
71
|
+
export const includesSubstring = (value: string): Constraint<string> => ({ _includesSubstring: value });
|
72
|
+
export const includesPattern = (value: string): Constraint<string> => ({ _includesPattern: value });
|
73
|
+
export const includesAll = (value: string): Constraint<string> => ({ _includesAll: value });
|
74
|
+
|
75
|
+
type FilterConstraints<T> = {
|
76
|
+
[key in keyof T]?: T[key] extends Record<string, any> ? FilterConstraints<T[key]> : T[key] | Constraint<T[key]>;
|
77
|
+
};
|
78
|
+
|
79
|
+
type BulkQueryOptions<T> = {
|
80
|
+
filter?: FilterConstraints<T>;
|
81
|
+
sort?:
|
82
|
+
| {
|
83
|
+
column: keyof T;
|
84
|
+
direction?: SortDirection;
|
85
|
+
}
|
86
|
+
| keyof T;
|
87
|
+
};
|
88
|
+
|
89
|
+
type QueryOrConstraint<T, R> = Query<T, R> | Constraint<T>;
|
90
|
+
|
91
|
+
export class Query<T, R = T> {
|
92
|
+
table: string;
|
93
|
+
repository: Repository<T>;
|
94
|
+
|
95
|
+
readonly _any?: QueryOrConstraint<T, R>[];
|
96
|
+
readonly _all?: QueryOrConstraint<T, R>[];
|
97
|
+
readonly _not?: QueryOrConstraint<T, R>[];
|
98
|
+
readonly _none?: QueryOrConstraint<T, R>[];
|
99
|
+
readonly _sort?: Record<string, SortDirection>;
|
100
|
+
|
101
|
+
constructor(repository: Repository<T> | null, table: string, data: Partial<Query<T, R>>, parent?: Query<T, R>) {
|
102
|
+
if (repository) {
|
103
|
+
this.repository = repository;
|
104
|
+
} else {
|
105
|
+
this.repository = this as any;
|
106
|
+
}
|
107
|
+
this.table = table;
|
108
|
+
|
109
|
+
// For some reason Object.assign(this, parent) didn't work in this case
|
110
|
+
// so doing all this manually:
|
111
|
+
this._any = parent?._any;
|
112
|
+
this._all = parent?._all;
|
113
|
+
this._not = parent?._not;
|
114
|
+
this._none = parent?._none;
|
115
|
+
this._sort = parent?._sort;
|
116
|
+
|
117
|
+
Object.assign(this, data);
|
118
|
+
// These bindings are used to support deconstructing
|
119
|
+
// const { any, not, filter, sort } = xata.users.query()
|
120
|
+
this.any = this.any.bind(this);
|
121
|
+
this.all = this.all.bind(this);
|
122
|
+
this.not = this.not.bind(this);
|
123
|
+
this.filter = this.filter.bind(this);
|
124
|
+
this.sort = this.sort.bind(this);
|
125
|
+
this.none = this.none.bind(this);
|
126
|
+
}
|
127
|
+
|
128
|
+
any(...queries: Query<T, R>[]): Query<T, R> {
|
129
|
+
return new Query<T, R>(
|
130
|
+
this.repository,
|
131
|
+
this.table,
|
132
|
+
{
|
133
|
+
_any: (this._any || []).concat(queries)
|
134
|
+
},
|
135
|
+
this
|
136
|
+
);
|
137
|
+
}
|
138
|
+
|
139
|
+
all(...queries: Query<T, R>[]): Query<T, R> {
|
140
|
+
return new Query<T, R>(
|
141
|
+
this.repository,
|
142
|
+
this.table,
|
143
|
+
{
|
144
|
+
_all: (this._all || []).concat(queries)
|
145
|
+
},
|
146
|
+
this
|
147
|
+
);
|
148
|
+
}
|
149
|
+
|
150
|
+
not(...queries: Query<T, R>[]): Query<T, R> {
|
151
|
+
return new Query<T, R>(
|
152
|
+
this.repository,
|
153
|
+
this.table,
|
154
|
+
{
|
155
|
+
_not: (this._not || []).concat(queries)
|
156
|
+
},
|
157
|
+
this
|
158
|
+
);
|
159
|
+
}
|
160
|
+
|
161
|
+
none(...queries: Query<T, R>[]): Query<T, R> {
|
162
|
+
return new Query<T, R>(
|
163
|
+
this.repository,
|
164
|
+
this.table,
|
165
|
+
{
|
166
|
+
_none: (this._none || []).concat(queries)
|
167
|
+
},
|
168
|
+
this
|
169
|
+
);
|
170
|
+
}
|
171
|
+
|
172
|
+
filter(constraints: FilterConstraints<T>): Query<T, R>;
|
173
|
+
filter<F extends keyof T>(column: F, value: FilterConstraints<T[F]> | Constraint<T[F]>): Query<T, R>;
|
174
|
+
filter(a: any, b?: any): Query<T, R> {
|
175
|
+
if (arguments.length === 1) {
|
176
|
+
const constraints = a as FilterConstraints<T>;
|
177
|
+
const queries: QueryOrConstraint<T, R>[] = [];
|
178
|
+
for (const [column, constraint] of Object.entries(constraints)) {
|
179
|
+
queries.push({ [column]: constraint });
|
180
|
+
}
|
181
|
+
return new Query<T, R>(
|
182
|
+
this.repository,
|
183
|
+
this.table,
|
184
|
+
{
|
185
|
+
_any: (this._any || []).concat(queries)
|
186
|
+
},
|
187
|
+
this
|
188
|
+
);
|
189
|
+
} else {
|
190
|
+
const column = a as keyof T;
|
191
|
+
const value = b as Partial<T[keyof T]> | Constraint<T[keyof T]>;
|
192
|
+
return new Query<T, R>(
|
193
|
+
this.repository,
|
194
|
+
this.table,
|
195
|
+
{
|
196
|
+
_any: (this._any || []).concat({ [column]: value })
|
197
|
+
},
|
198
|
+
this
|
199
|
+
);
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
sort<F extends keyof T>(column: F, direction: SortDirection): Query<T, R> {
|
204
|
+
const sort = { ...this._sort, [column]: direction };
|
205
|
+
const q = new Query<T, R>(
|
206
|
+
this.repository,
|
207
|
+
this.table,
|
208
|
+
{
|
209
|
+
_sort: sort
|
210
|
+
},
|
211
|
+
this
|
212
|
+
);
|
213
|
+
|
214
|
+
return q;
|
215
|
+
}
|
216
|
+
|
217
|
+
// TODO: pagination. Maybe implement different methods for different type of paginations
|
218
|
+
// and one to simply get the first records returned by the query with no pagination.
|
219
|
+
async getMany(options?: BulkQueryOptions<T>): Promise<R[]> {
|
220
|
+
// TODO: use options
|
221
|
+
return this.repository.query(this);
|
222
|
+
}
|
223
|
+
|
224
|
+
async getOne(options?: BulkQueryOptions<T>): Promise<R | null> {
|
225
|
+
// TODO: use options
|
226
|
+
const arr = await this.getMany(); // TODO, limit to 1
|
227
|
+
return arr[0] || null;
|
228
|
+
}
|
229
|
+
|
230
|
+
async deleteAll(): Promise<number> {
|
231
|
+
// Return number of affected rows
|
232
|
+
return 0;
|
233
|
+
}
|
234
|
+
|
235
|
+
include(columns: Include<T>) {
|
236
|
+
// TODO
|
237
|
+
return this;
|
238
|
+
}
|
239
|
+
|
240
|
+
toJSON() {
|
241
|
+
const _filter = {
|
242
|
+
_any: this._any,
|
243
|
+
_all: this._all,
|
244
|
+
_not: this._not,
|
245
|
+
_none: this._none
|
246
|
+
};
|
247
|
+
return {
|
248
|
+
_filter: Object.values(_filter).some(Boolean) ? _filter : undefined,
|
249
|
+
_sort: this._sort
|
250
|
+
};
|
251
|
+
}
|
252
|
+
}
|
253
|
+
|
254
|
+
export abstract class Repository<T> extends Query<T, Selectable<T>> {
|
255
|
+
select<K extends keyof Selectable<T>>(...columns: K[]) {
|
256
|
+
return new Query<T, Select<T, K>>(this.repository, this.table, {});
|
257
|
+
}
|
258
|
+
|
259
|
+
abstract create(object: Selectable<T>): Promise<T>;
|
260
|
+
|
261
|
+
abstract read(id: string): Promise<T | null>;
|
262
|
+
|
263
|
+
abstract update(id: string, object: Partial<T>): Promise<T>;
|
264
|
+
|
265
|
+
abstract delete(id: string): void;
|
266
|
+
|
267
|
+
// Used by the Query object internally
|
268
|
+
abstract query<R>(query: Query<T, R>): Promise<R[]>;
|
269
|
+
}
|
270
|
+
|
271
|
+
export class RestRepository<T> extends Repository<T> {
|
272
|
+
client: BaseClient<any>;
|
273
|
+
fetch: any;
|
274
|
+
|
275
|
+
constructor(client: BaseClient<any>, table: string) {
|
276
|
+
super(null, table, {});
|
277
|
+
this.client = client;
|
278
|
+
|
279
|
+
const { fetch } = client.options;
|
280
|
+
|
281
|
+
if (fetch) {
|
282
|
+
this.fetch = fetch;
|
283
|
+
} else if (typeof window === 'object') {
|
284
|
+
this.fetch = window.fetch;
|
285
|
+
} else if (typeof require === 'function') {
|
286
|
+
try {
|
287
|
+
this.fetch = require('node-fetch');
|
288
|
+
} catch (err) {
|
289
|
+
try {
|
290
|
+
this.fetch = require('cross-fetch');
|
291
|
+
} catch (err) {
|
292
|
+
throw new Error('No fetch implementation found. Please provide one in the constructor');
|
293
|
+
}
|
294
|
+
}
|
295
|
+
}
|
296
|
+
|
297
|
+
Object.defineProperty(this, 'client', { enumerable: false });
|
298
|
+
Object.defineProperty(this, 'fetch', { enumerable: false });
|
299
|
+
Object.defineProperty(this, 'hostname', { enumerable: false });
|
300
|
+
}
|
301
|
+
|
302
|
+
async request(method: string, path: string, body?: unknown) {
|
303
|
+
const { url: xatabaseURL } = this.client.options;
|
304
|
+
const resp: Response = await this.fetch(`${xatabaseURL}${path}`, {
|
305
|
+
method,
|
306
|
+
headers: {
|
307
|
+
Accept: '*/*',
|
308
|
+
'Content-Type': 'application/json',
|
309
|
+
Authorization: `Bearer ${this.client.options.token}`
|
310
|
+
},
|
311
|
+
body: JSON.stringify(body)
|
312
|
+
});
|
313
|
+
if (!resp.ok) {
|
314
|
+
try {
|
315
|
+
const json = await resp.json();
|
316
|
+
const message = json.message;
|
317
|
+
if (typeof message === 'string') {
|
318
|
+
throw new XataError(message, resp.status);
|
319
|
+
}
|
320
|
+
} catch (err) {
|
321
|
+
if (err instanceof XataError) throw err;
|
322
|
+
// Ignore errors for other reasons.
|
323
|
+
// For example if the response's body cannot be parsed as JSON
|
324
|
+
}
|
325
|
+
throw new XataError(resp.statusText, resp.status);
|
326
|
+
}
|
327
|
+
if (resp.status === 204) return;
|
328
|
+
return resp.json();
|
329
|
+
}
|
330
|
+
|
331
|
+
select<K extends keyof T>(...columns: K[]) {
|
332
|
+
return new Query<T, Select<T, K>>(this.repository, this.table, {});
|
333
|
+
}
|
334
|
+
|
335
|
+
async create(object: T): Promise<T> {
|
336
|
+
const obj = await this.request('POST', `/tables/${this.table}/data`, object);
|
337
|
+
return this.client.initObject(this.table, obj);
|
338
|
+
}
|
339
|
+
|
340
|
+
async read(id: string): Promise<T | null> {
|
341
|
+
try {
|
342
|
+
const obj = await this.request('GET', `/tables/${this.table}/data/${id}`);
|
343
|
+
return this.client.initObject(this.table, obj);
|
344
|
+
} catch (err) {
|
345
|
+
if ((err as XataError).status === 404) return null;
|
346
|
+
throw err;
|
347
|
+
}
|
348
|
+
}
|
349
|
+
|
350
|
+
async update(id: string, object: Partial<T>): Promise<T> {
|
351
|
+
const obj = await this.request('PUT', `/tables/${this.table}/data/${id}`, object);
|
352
|
+
return this.client.initObject(this.table, obj);
|
353
|
+
}
|
354
|
+
|
355
|
+
async delete(id: string) {
|
356
|
+
await this.request('DELETE', `/tables/${this.table}/data/${id}`);
|
357
|
+
}
|
358
|
+
|
359
|
+
async query<R>(query: Query<T, R>): Promise<R[]> {
|
360
|
+
const result = await this.request('POST', `/tables/${this.table}/query`, query);
|
361
|
+
return result.records.map((record: object) => this.client.initObject(this.table, record));
|
362
|
+
}
|
363
|
+
}
|
364
|
+
|
365
|
+
interface RepositoryFactory {
|
366
|
+
createRepository<T>(client: BaseClient<any>, table: string): Repository<T>;
|
367
|
+
}
|
368
|
+
|
369
|
+
export class RestRespositoryFactory implements RepositoryFactory {
|
370
|
+
createRepository<T>(client: BaseClient<any>, table: string): Repository<T> {
|
371
|
+
return new RestRepository<T>(client, table);
|
372
|
+
}
|
373
|
+
}
|
374
|
+
|
375
|
+
export type XataClientOptions = {
|
376
|
+
fetch?: unknown;
|
377
|
+
url: string;
|
378
|
+
token: string;
|
379
|
+
repositoryFactory?: RepositoryFactory;
|
380
|
+
};
|
381
|
+
|
382
|
+
export class BaseClient<D extends Record<string, Repository<any>>> {
|
383
|
+
options: XataClientOptions;
|
384
|
+
private links: Links;
|
385
|
+
db!: D;
|
386
|
+
|
387
|
+
constructor(options: XataClientOptions, links: Links) {
|
388
|
+
this.options = options;
|
389
|
+
this.links = links;
|
390
|
+
}
|
391
|
+
|
392
|
+
public initObject<T>(table: string, object: object) {
|
393
|
+
const o: Record<string, unknown> = {};
|
394
|
+
Object.assign(o, object);
|
395
|
+
|
396
|
+
const tableLinks = this.links[table] || [];
|
397
|
+
for (const link of tableLinks) {
|
398
|
+
const [field, linkTable] = link;
|
399
|
+
const value = o[field];
|
400
|
+
|
401
|
+
if (value && typeof value === 'object') {
|
402
|
+
const { _id } = value as any;
|
403
|
+
if (Object.keys(value).find((col) => !col.startsWith('_'))) {
|
404
|
+
o[field] = this.initObject(linkTable, value);
|
405
|
+
} else if (_id) {
|
406
|
+
o[field] = {
|
407
|
+
_id,
|
408
|
+
get: () => {
|
409
|
+
this.db[linkTable].read(_id);
|
410
|
+
}
|
411
|
+
};
|
412
|
+
}
|
413
|
+
}
|
414
|
+
}
|
415
|
+
|
416
|
+
const db = this.db;
|
417
|
+
o.read = function () {
|
418
|
+
return db[table].read(o['_id'] as string);
|
419
|
+
};
|
420
|
+
o.update = function (data: any) {
|
421
|
+
return db[table].update(o['_id'] as string, data);
|
422
|
+
};
|
423
|
+
o.delete = function () {
|
424
|
+
return db[table].delete(o['_id'] as string);
|
425
|
+
};
|
426
|
+
|
427
|
+
for (const prop of ['read', 'update', 'delete']) {
|
428
|
+
Object.defineProperty(o, prop, { enumerable: false });
|
429
|
+
}
|
430
|
+
|
431
|
+
// TODO: links and rev links
|
432
|
+
|
433
|
+
Object.freeze(o);
|
434
|
+
return o as T;
|
435
|
+
}
|
436
|
+
}
|
437
|
+
|
438
|
+
export class XataError extends Error {
|
439
|
+
readonly status: number;
|
440
|
+
|
441
|
+
constructor(message: string, status: number) {
|
442
|
+
super(message);
|
443
|
+
this.status = status;
|
444
|
+
}
|
445
|
+
}
|
446
|
+
|
447
|
+
export type Links = Record<string, Array<string[]>>;
|
package/tsconfig.json
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "ES6",
|
4
|
+
"lib": ["dom", "esnext"],
|
5
|
+
"allowJs": true,
|
6
|
+
"skipLibCheck": true,
|
7
|
+
"esModuleInterop": true,
|
8
|
+
"allowSyntheticDefaultImports": true,
|
9
|
+
"strict": true,
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
12
|
+
"module": "CommonJS",
|
13
|
+
"moduleResolution": "node",
|
14
|
+
"resolveJsonModule": true,
|
15
|
+
"isolatedModules": true,
|
16
|
+
"noEmit": false,
|
17
|
+
"outDir": "dist",
|
18
|
+
"declaration": true
|
19
|
+
},
|
20
|
+
"include": ["src"]
|
21
|
+
}
|