@awesomate/sdk 0.1.0 → 0.3.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/README.md +24 -0
- package/dist/index.d.ts +167 -3
- package/dist/index.js +74 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,30 @@ const one = await db.get('contact', rows[0].id); // null if there is none
|
|
|
30
30
|
const byPlan = await db.aggregate({ measures: [{ column: 'people', agg: 'count' }], dimensions: ['plan'] });
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Writing app data (Support Plus and above)
|
|
34
|
+
|
|
35
|
+
Define the tables an app keeps, then write to them. There's no migration and no SQL; every value is
|
|
36
|
+
checked against the kind (types, choices, required attributes and links).
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
await db.defineKind({
|
|
40
|
+
key: 'job', label: 'Job', label_plural: 'Jobs',
|
|
41
|
+
attributes: [
|
|
42
|
+
{ key: 'title', type: 'text', required: true, readable_by_ai: true },
|
|
43
|
+
{ key: 'status', type: 'choice', choices: ['open', 'booked', 'done'], readable_by_ai: true },
|
|
44
|
+
],
|
|
45
|
+
links: [{ key: 'customer', to: 'contact' }],
|
|
46
|
+
});
|
|
47
|
+
const id = await db.write('job', { title: 'Possum in the roof', status: 'open' }, { links: { customer: contactId } });
|
|
48
|
+
await db.write('job', { status: 'booked' }, { id }); // values not given stay
|
|
49
|
+
const jobs = await db.query('job', { where: { customer_id: contactId } });
|
|
50
|
+
await db.archive('job', id);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Only attributes marked `readable_by_ai` come back from `query` and `get`; a `sensitive` attribute
|
|
54
|
+
can never be readable. Contacts themselves are not written this way: the contact list keeps its
|
|
55
|
+
own consent rules.
|
|
56
|
+
|
|
33
57
|
## What you can read
|
|
34
58
|
|
|
35
59
|
The built-in contact details (name, email, phone, company, tags, when they were added) and only
|
package/dist/index.d.ts
CHANGED
|
@@ -6,15 +6,37 @@
|
|
|
6
6
|
* const db = createClient({ token: process.env.AWESOMATE_TOKEN! });
|
|
7
7
|
* const { rows, next } = await db.query('contact', { where: { created_at: { gte: '$YEAR_BEGIN' } }, limit: 50 });
|
|
8
8
|
*
|
|
9
|
+
* const open = await db.run('open_jobs_in', { suburb: 'Carindale' }); // a saved query, by name
|
|
10
|
+
* await db.call('log_visit', { customer, title: 'Possum in the roof' }); // a write recipe, one transaction
|
|
11
|
+
*
|
|
9
12
|
* The account's token (a hosting PAT with crm:read) is a secret: use it on a server, never in a
|
|
10
|
-
* browser. End-user sign-in for browser apps comes in a later version.
|
|
13
|
+
* browser. End-user sign-in for browser apps comes in a later version. Reading and running saved
|
|
14
|
+
* queries works on every plan; writing app data (kinds, records, saved queries, recipes) needs
|
|
15
|
+
* crm:write, Support Plus and above.
|
|
11
16
|
*/
|
|
12
|
-
export declare const VERSION = "0.
|
|
17
|
+
export declare const VERSION = "0.3.0";
|
|
13
18
|
/** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
|
|
14
19
|
export interface Kinds {
|
|
15
20
|
}
|
|
16
21
|
export type KindName = [keyof Kinds] extends [never] ? string : Extract<keyof Kinds, string>;
|
|
17
22
|
export type RowOf<K> = K extends keyof Kinds ? Kinds[K] : Record<string, unknown>;
|
|
23
|
+
/** Augmented by the generated awesomate.d.ts: each saved query's params and row. */
|
|
24
|
+
export interface Queries {
|
|
25
|
+
}
|
|
26
|
+
/** Augmented by the generated awesomate.d.ts: each write recipe's args. */
|
|
27
|
+
export interface Recipes {
|
|
28
|
+
}
|
|
29
|
+
export type QueryName = [keyof Queries] extends [never] ? string : Extract<keyof Queries, string>;
|
|
30
|
+
export type ParamsOf<Q> = Q extends keyof Queries ? (Queries[Q] extends {
|
|
31
|
+
params: infer P;
|
|
32
|
+
} ? P : never) : Record<string, unknown>;
|
|
33
|
+
export type QueryRowOf<Q> = Q extends keyof Queries ? (Queries[Q] extends {
|
|
34
|
+
row: infer R;
|
|
35
|
+
} ? R : never) : Record<string, unknown>;
|
|
36
|
+
export type RecipeName = [keyof Recipes] extends [never] ? string : Extract<keyof Recipes, string>;
|
|
37
|
+
export type ArgsOf<R> = R extends keyof Recipes ? (Recipes[R] extends {
|
|
38
|
+
args: infer A;
|
|
39
|
+
} ? A : never) : Record<string, unknown>;
|
|
18
40
|
type TextOps<V extends string> = {
|
|
19
41
|
eq?: V | null;
|
|
20
42
|
neq?: V | null;
|
|
@@ -122,7 +144,7 @@ export declare class AwesomateClient {
|
|
|
122
144
|
private readonly base;
|
|
123
145
|
private readonly doFetch;
|
|
124
146
|
constructor(opts: ClientOptions);
|
|
125
|
-
private
|
|
147
|
+
private request;
|
|
126
148
|
/** The kinds this token can read, their columns, operators and examples. */
|
|
127
149
|
schema(): Promise<{
|
|
128
150
|
kinds: Array<{
|
|
@@ -151,6 +173,42 @@ export declare class AwesomateClient {
|
|
|
151
173
|
get<K extends KindName>(kind: K, id: string, options?: {
|
|
152
174
|
tz?: string;
|
|
153
175
|
}): Promise<RowOf<K> | null>;
|
|
176
|
+
/** The app kinds this account has defined. */
|
|
177
|
+
kinds(): Promise<KindDescription[]>;
|
|
178
|
+
/** Define an app kind (a table an app keeps). No migration: registry rows and generated views. */
|
|
179
|
+
defineKind(spec: KindSpec): Promise<KindDescription>;
|
|
180
|
+
/** Create a record (returns its id), or change one with options.id. Every value is checked against the kind. */
|
|
181
|
+
write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
|
|
182
|
+
/** Archive a record: it leaves every read and is kept for a restore. */
|
|
183
|
+
archive<K extends KindName>(kind: K, id: string): Promise<void>;
|
|
184
|
+
/** The saved queries on this account. */
|
|
185
|
+
queries(): Promise<SavedQueryDescription[]>;
|
|
186
|
+
/**
|
|
187
|
+
* Save a query by name (saving a name again replaces it). The spec is the query() grammar on one
|
|
188
|
+
* kind, with {"$param": "<name>"} where a caller's value goes; it is compiled before it is
|
|
189
|
+
* stored, so a column it cannot read is refused now rather than when it runs.
|
|
190
|
+
*/
|
|
191
|
+
saveQuery(spec: SavedQuerySpec): Promise<SavedQueryDescription>;
|
|
192
|
+
archiveQuery(key: string): Promise<void>;
|
|
193
|
+
/** Run a saved query with its params. An optional param left out drops its condition. */
|
|
194
|
+
run<Q extends QueryName>(query: Q, params?: ParamsOf<Q>, options?: {
|
|
195
|
+
limit?: number;
|
|
196
|
+
after?: string;
|
|
197
|
+
tz?: string;
|
|
198
|
+
}): Promise<Page<QueryRowOf<Q>>>;
|
|
199
|
+
/** The write recipes on this account. */
|
|
200
|
+
recipes(): Promise<RecipeDescription[]>;
|
|
201
|
+
/**
|
|
202
|
+
* Save a write recipe: up to 10 write_record / archive_record steps run in one transaction.
|
|
203
|
+
* {"$param": "<name>"} takes a caller's value, {"$step": "<as>"} the id an earlier step wrote.
|
|
204
|
+
*/
|
|
205
|
+
saveRecipe(spec: RecipeSpec): Promise<RecipeDescription>;
|
|
206
|
+
archiveRecipe(key: string): Promise<void>;
|
|
207
|
+
/**
|
|
208
|
+
* Run a write recipe. Every step commits or none does; a refusal names the step and the field.
|
|
209
|
+
* Never retried: a write that may have landed is not safe to send twice.
|
|
210
|
+
*/
|
|
211
|
+
call<R extends RecipeName>(recipe: R, args?: ArgsOf<R>): Promise<RecipeResult>;
|
|
154
212
|
/** Grouped numbers (counts, sums) in the Business Data API's shape. */
|
|
155
213
|
aggregate(request: {
|
|
156
214
|
measures: Array<{
|
|
@@ -178,5 +236,111 @@ export declare class AwesomateClient {
|
|
|
178
236
|
limit?: number;
|
|
179
237
|
}): Promise<Record<string, unknown>>;
|
|
180
238
|
}
|
|
239
|
+
export interface AttributeSpec {
|
|
240
|
+
key: string;
|
|
241
|
+
label?: string;
|
|
242
|
+
type?: 'text' | 'long_text' | 'number' | 'money' | 'date' | 'datetime' | 'duration' | 'yes_no' | 'choice' | 'choices' | 'email' | 'phone' | 'address' | 'url' | 'file';
|
|
243
|
+
choices?: string[];
|
|
244
|
+
required?: boolean;
|
|
245
|
+
sensitivity?: 'ordinary' | 'personal' | 'sensitive';
|
|
246
|
+
/** Default false. Only readable attributes reach query(), get() and the generated types. */
|
|
247
|
+
readable_by_ai?: boolean;
|
|
248
|
+
}
|
|
249
|
+
export interface KindSpec {
|
|
250
|
+
key: string;
|
|
251
|
+
label?: string;
|
|
252
|
+
label_plural?: string;
|
|
253
|
+
attributes: AttributeSpec[];
|
|
254
|
+
/** to is 'contact' or a kind already defined; each link reads back as <key>_id. */
|
|
255
|
+
links?: Array<{
|
|
256
|
+
key: string;
|
|
257
|
+
to: string;
|
|
258
|
+
label?: string;
|
|
259
|
+
to_label?: string;
|
|
260
|
+
required?: boolean;
|
|
261
|
+
}>;
|
|
262
|
+
}
|
|
263
|
+
export interface KindDescription extends Omit<KindSpec, 'attributes'> {
|
|
264
|
+
storage: 'plain' | 'tracked';
|
|
265
|
+
attributes: Array<Required<Pick<AttributeSpec, 'key' | 'label' | 'type' | 'required' | 'sensitivity' | 'readable_by_ai'>> & {
|
|
266
|
+
choices: string[] | null;
|
|
267
|
+
}>;
|
|
268
|
+
views: {
|
|
269
|
+
all: string;
|
|
270
|
+
for_agents: string;
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
export interface WriteOptions {
|
|
274
|
+
/** Change this record instead of creating one; values not given stay. */
|
|
275
|
+
id?: string;
|
|
276
|
+
/** Set a link by its key ('<id>'), or end it (null). */
|
|
277
|
+
links?: Record<string, string | null>;
|
|
278
|
+
}
|
|
279
|
+
export type ParamType = 'text' | 'number' | 'date' | 'datetime' | 'boolean' | 'uuid' | 'text_list' | 'number_list';
|
|
280
|
+
export interface ParamSpec {
|
|
281
|
+
name: string;
|
|
282
|
+
/** Default text. */
|
|
283
|
+
type?: ParamType;
|
|
284
|
+
required?: boolean;
|
|
285
|
+
default?: unknown;
|
|
286
|
+
label?: string;
|
|
287
|
+
}
|
|
288
|
+
/** Where a caller's value goes in a saved query or a recipe. */
|
|
289
|
+
export type Param = {
|
|
290
|
+
$param: string;
|
|
291
|
+
};
|
|
292
|
+
export interface SavedQuerySpec {
|
|
293
|
+
key: string;
|
|
294
|
+
label: string;
|
|
295
|
+
description?: string;
|
|
296
|
+
/** 'contact' or an app kind. */
|
|
297
|
+
kind: string;
|
|
298
|
+
spec: {
|
|
299
|
+
where?: Record<string, unknown>;
|
|
300
|
+
orderBy?: Array<[string, 'asc' | 'desc']>;
|
|
301
|
+
select?: string[];
|
|
302
|
+
limit?: number;
|
|
303
|
+
};
|
|
304
|
+
params?: ParamSpec[];
|
|
305
|
+
}
|
|
306
|
+
export interface SavedQueryDescription extends Required<Omit<SavedQuerySpec, 'description' | 'params'>> {
|
|
307
|
+
description: string | null;
|
|
308
|
+
params: ParamSpec[];
|
|
309
|
+
updated_at: string;
|
|
310
|
+
}
|
|
311
|
+
export type RecipeStep = {
|
|
312
|
+
op: 'write_record';
|
|
313
|
+
kind: string;
|
|
314
|
+
id?: Param | {
|
|
315
|
+
$step: string;
|
|
316
|
+
};
|
|
317
|
+
as?: string;
|
|
318
|
+
data?: Record<string, unknown>;
|
|
319
|
+
links?: Record<string, unknown>;
|
|
320
|
+
} | {
|
|
321
|
+
op: 'archive_record';
|
|
322
|
+
kind: string;
|
|
323
|
+
id: Param | {
|
|
324
|
+
$step: string;
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
export interface RecipeSpec {
|
|
328
|
+
key: string;
|
|
329
|
+
label: string;
|
|
330
|
+
description?: string;
|
|
331
|
+
params?: ParamSpec[];
|
|
332
|
+
steps: RecipeStep[];
|
|
333
|
+
}
|
|
334
|
+
export interface RecipeDescription extends Required<Omit<RecipeSpec, 'description' | 'params'>> {
|
|
335
|
+
description: string | null;
|
|
336
|
+
params: ParamSpec[];
|
|
337
|
+
updated_at: string;
|
|
338
|
+
}
|
|
339
|
+
export interface RecipeResult {
|
|
340
|
+
/** The id each step named with `as` wrote. */
|
|
341
|
+
ids: Record<string, string>;
|
|
342
|
+
/** Every record the recipe wrote or archived, in step order. */
|
|
343
|
+
records: string[];
|
|
344
|
+
}
|
|
181
345
|
export declare function createClient(options: ClientOptions): AwesomateClient;
|
|
182
346
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
* const db = createClient({ token: process.env.AWESOMATE_TOKEN! });
|
|
7
7
|
* const { rows, next } = await db.query('contact', { where: { created_at: { gte: '$YEAR_BEGIN' } }, limit: 50 });
|
|
8
8
|
*
|
|
9
|
+
* const open = await db.run('open_jobs_in', { suburb: 'Carindale' }); // a saved query, by name
|
|
10
|
+
* await db.call('log_visit', { customer, title: 'Possum in the roof' }); // a write recipe, one transaction
|
|
11
|
+
*
|
|
9
12
|
* The account's token (a hosting PAT with crm:read) is a secret: use it on a server, never in a
|
|
10
|
-
* browser. End-user sign-in for browser apps comes in a later version.
|
|
13
|
+
* browser. End-user sign-in for browser apps comes in a later version. Reading and running saved
|
|
14
|
+
* queries works on every plan; writing app data (kinds, records, saved queries, recipes) needs
|
|
15
|
+
* crm:write, Support Plus and above.
|
|
11
16
|
*/
|
|
12
|
-
export const VERSION = '0.
|
|
17
|
+
export const VERSION = '0.3.0';
|
|
13
18
|
const DEFAULT_BASE = 'https://hub.awesomate.ai';
|
|
14
19
|
export class AwesomateError extends Error {
|
|
15
20
|
code;
|
|
@@ -57,7 +62,7 @@ export class AwesomateClient {
|
|
|
57
62
|
if (!this.doFetch)
|
|
58
63
|
throw new Error('No fetch available: use Node 18 or later, or pass fetch.');
|
|
59
64
|
}
|
|
60
|
-
async
|
|
65
|
+
async request(method, path, body, retry = true) {
|
|
61
66
|
const res = await this.doFetch(`${this.base}${path}`, {
|
|
62
67
|
method,
|
|
63
68
|
headers: {
|
|
@@ -80,20 +85,20 @@ export class AwesomateClient {
|
|
|
80
85
|
// The field list changed while the read ran (the view is rebuilt on every field edit): a read
|
|
81
86
|
// is safe to run again, once.
|
|
82
87
|
if (code === 'conflict' && retry)
|
|
83
|
-
return this.
|
|
88
|
+
return this.request(method, path, body, false);
|
|
84
89
|
const message = typeof json.error === 'string' ? json.error : `The hub answered ${res.status}.`;
|
|
85
90
|
throw new AwesomateError(code, message, res.status, typeof json.field === 'string' ? json.field : undefined, server);
|
|
86
91
|
}
|
|
87
92
|
/** The kinds this token can read, their columns, operators and examples. */
|
|
88
93
|
schema() {
|
|
89
|
-
return this.
|
|
94
|
+
return this.request('GET', '/api/my-crm/v1/rows/schema');
|
|
90
95
|
}
|
|
91
96
|
/** The readable kinds as a TypeScript file (what the types command writes). */
|
|
92
97
|
types() {
|
|
93
|
-
return this.
|
|
98
|
+
return this.request('GET', '/api/my-crm/v1/rows/types');
|
|
94
99
|
}
|
|
95
100
|
async query(kind, options = {}) {
|
|
96
|
-
const r = await this.
|
|
101
|
+
const r = await this.request('POST', '/api/my-crm/v1/rows/query', { kind, ...options });
|
|
97
102
|
return { rows: r.rows, next: r.next, count: r.count, applied: r.applied, hidden: r.hidden, asAt: r.as_at };
|
|
98
103
|
}
|
|
99
104
|
/** Every matching row, a page at a time. maxRows guards against reading a whole list by accident. */
|
|
@@ -115,7 +120,7 @@ export class AwesomateClient {
|
|
|
115
120
|
async get(kind, id, options = {}) {
|
|
116
121
|
try {
|
|
117
122
|
const qs = options.tz ? `?tz=${encodeURIComponent(options.tz)}` : '';
|
|
118
|
-
const r = await this.
|
|
123
|
+
const r = await this.request('GET', `/api/my-crm/v1/rows/${encodeURIComponent(kind)}/${encodeURIComponent(id)}${qs}`);
|
|
119
124
|
return r.row;
|
|
120
125
|
}
|
|
121
126
|
catch (err) {
|
|
@@ -124,9 +129,69 @@ export class AwesomateClient {
|
|
|
124
129
|
throw err;
|
|
125
130
|
}
|
|
126
131
|
}
|
|
132
|
+
/** The app kinds this account has defined. */
|
|
133
|
+
async kinds() {
|
|
134
|
+
return (await this.request('GET', '/api/my-crm/v1/kinds')).kinds;
|
|
135
|
+
}
|
|
136
|
+
/** Define an app kind (a table an app keeps). No migration: registry rows and generated views. */
|
|
137
|
+
async defineKind(spec) {
|
|
138
|
+
return (await this.request('POST', '/api/my-crm/v1/kinds', spec)).kind;
|
|
139
|
+
}
|
|
140
|
+
/** Create a record (returns its id), or change one with options.id. Every value is checked against the kind. */
|
|
141
|
+
async write(kind, data, options = {}) {
|
|
142
|
+
const r = await this.request('POST', '/api/my-crm/v1/call', {
|
|
143
|
+
fn: 'write_record', args: { kind, data, links: options.links ?? {}, ...(options.id ? { id: options.id } : {}) },
|
|
144
|
+
}, false);
|
|
145
|
+
return r.result.id;
|
|
146
|
+
}
|
|
147
|
+
/** Archive a record: it leaves every read and is kept for a restore. */
|
|
148
|
+
async archive(kind, id) {
|
|
149
|
+
await this.request('POST', '/api/my-crm/v1/call', { fn: 'archive_record', args: { kind, id } }, false);
|
|
150
|
+
}
|
|
151
|
+
/** The saved queries on this account. */
|
|
152
|
+
async queries() {
|
|
153
|
+
return (await this.request('GET', '/api/my-crm/v1/queries')).queries;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Save a query by name (saving a name again replaces it). The spec is the query() grammar on one
|
|
157
|
+
* kind, with {"$param": "<name>"} where a caller's value goes; it is compiled before it is
|
|
158
|
+
* stored, so a column it cannot read is refused now rather than when it runs.
|
|
159
|
+
*/
|
|
160
|
+
async saveQuery(spec) {
|
|
161
|
+
return (await this.request('POST', '/api/my-crm/v1/queries', spec, false)).query;
|
|
162
|
+
}
|
|
163
|
+
async archiveQuery(key) {
|
|
164
|
+
await this.request('DELETE', `/api/my-crm/v1/queries/${encodeURIComponent(key)}`, undefined, false);
|
|
165
|
+
}
|
|
166
|
+
/** Run a saved query with its params. An optional param left out drops its condition. */
|
|
167
|
+
async run(query, params = {}, options = {}) {
|
|
168
|
+
const r = await this.request('POST', `/api/my-crm/v1/queries/${encodeURIComponent(query)}/run`, { params, ...options });
|
|
169
|
+
return { rows: r.rows, next: r.next, count: r.count, applied: r.applied, hidden: r.hidden, asAt: r.as_at };
|
|
170
|
+
}
|
|
171
|
+
/** The write recipes on this account. */
|
|
172
|
+
async recipes() {
|
|
173
|
+
return (await this.request('GET', '/api/my-crm/v1/recipes')).recipes;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Save a write recipe: up to 10 write_record / archive_record steps run in one transaction.
|
|
177
|
+
* {"$param": "<name>"} takes a caller's value, {"$step": "<as>"} the id an earlier step wrote.
|
|
178
|
+
*/
|
|
179
|
+
async saveRecipe(spec) {
|
|
180
|
+
return (await this.request('POST', '/api/my-crm/v1/recipes', spec, false)).recipe;
|
|
181
|
+
}
|
|
182
|
+
async archiveRecipe(key) {
|
|
183
|
+
await this.request('DELETE', `/api/my-crm/v1/recipes/${encodeURIComponent(key)}`, undefined, false);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Run a write recipe. Every step commits or none does; a refusal names the step and the field.
|
|
187
|
+
* Never retried: a write that may have landed is not safe to send twice.
|
|
188
|
+
*/
|
|
189
|
+
async call(recipe, args = {}) {
|
|
190
|
+
return (await this.request('POST', '/api/my-crm/v1/call', { fn: recipe, args }, false)).result;
|
|
191
|
+
}
|
|
127
192
|
/** Grouped numbers (counts, sums) in the Business Data API's shape. */
|
|
128
193
|
aggregate(request) {
|
|
129
|
-
return this.
|
|
194
|
+
return this.request('POST', '/api/my-crm/v1/data/query', request);
|
|
130
195
|
}
|
|
131
196
|
}
|
|
132
197
|
export function createClient(options) {
|
package/package.json
CHANGED