@awesomate/sdk 0.2.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/dist/index.d.ts CHANGED
@@ -6,16 +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. Writing app data (kinds and
11
- * their records) needs crm:write, Support Plus and above.
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.
12
16
  */
13
- export declare const VERSION = "0.2.0";
17
+ export declare const VERSION = "0.3.0";
14
18
  /** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
15
19
  export interface Kinds {
16
20
  }
17
21
  export type KindName = [keyof Kinds] extends [never] ? string : Extract<keyof Kinds, string>;
18
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>;
19
40
  type TextOps<V extends string> = {
20
41
  eq?: V | null;
21
42
  neq?: V | null;
@@ -123,7 +144,7 @@ export declare class AwesomateClient {
123
144
  private readonly base;
124
145
  private readonly doFetch;
125
146
  constructor(opts: ClientOptions);
126
- private call;
147
+ private request;
127
148
  /** The kinds this token can read, their columns, operators and examples. */
128
149
  schema(): Promise<{
129
150
  kinds: Array<{
@@ -160,6 +181,34 @@ export declare class AwesomateClient {
160
181
  write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
161
182
  /** Archive a record: it leaves every read and is kept for a restore. */
162
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>;
163
212
  /** Grouped numbers (counts, sums) in the Business Data API's shape. */
164
213
  aggregate(request: {
165
214
  measures: Array<{
@@ -227,5 +276,71 @@ export interface WriteOptions {
227
276
  /** Set a link by its key ('<id>'), or end it (null). */
228
277
  links?: Record<string, string | null>;
229
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
+ }
230
345
  export declare function createClient(options: ClientOptions): AwesomateClient;
231
346
  export {};
package/dist/index.js CHANGED
@@ -6,11 +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. Writing app data (kinds and
11
- * their records) needs crm:write, Support Plus and above.
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.
12
16
  */
13
- export const VERSION = '0.2.0';
17
+ export const VERSION = '0.3.0';
14
18
  const DEFAULT_BASE = 'https://hub.awesomate.ai';
15
19
  export class AwesomateError extends Error {
16
20
  code;
@@ -58,7 +62,7 @@ export class AwesomateClient {
58
62
  if (!this.doFetch)
59
63
  throw new Error('No fetch available: use Node 18 or later, or pass fetch.');
60
64
  }
61
- async call(method, path, body, retry = true) {
65
+ async request(method, path, body, retry = true) {
62
66
  const res = await this.doFetch(`${this.base}${path}`, {
63
67
  method,
64
68
  headers: {
@@ -81,20 +85,20 @@ export class AwesomateClient {
81
85
  // The field list changed while the read ran (the view is rebuilt on every field edit): a read
82
86
  // is safe to run again, once.
83
87
  if (code === 'conflict' && retry)
84
- return this.call(method, path, body, false);
88
+ return this.request(method, path, body, false);
85
89
  const message = typeof json.error === 'string' ? json.error : `The hub answered ${res.status}.`;
86
90
  throw new AwesomateError(code, message, res.status, typeof json.field === 'string' ? json.field : undefined, server);
87
91
  }
88
92
  /** The kinds this token can read, their columns, operators and examples. */
89
93
  schema() {
90
- return this.call('GET', '/api/my-crm/v1/rows/schema');
94
+ return this.request('GET', '/api/my-crm/v1/rows/schema');
91
95
  }
92
96
  /** The readable kinds as a TypeScript file (what the types command writes). */
93
97
  types() {
94
- return this.call('GET', '/api/my-crm/v1/rows/types');
98
+ return this.request('GET', '/api/my-crm/v1/rows/types');
95
99
  }
96
100
  async query(kind, options = {}) {
97
- const r = await this.call('POST', '/api/my-crm/v1/rows/query', { kind, ...options });
101
+ const r = await this.request('POST', '/api/my-crm/v1/rows/query', { kind, ...options });
98
102
  return { rows: r.rows, next: r.next, count: r.count, applied: r.applied, hidden: r.hidden, asAt: r.as_at };
99
103
  }
100
104
  /** Every matching row, a page at a time. maxRows guards against reading a whole list by accident. */
@@ -116,7 +120,7 @@ export class AwesomateClient {
116
120
  async get(kind, id, options = {}) {
117
121
  try {
118
122
  const qs = options.tz ? `?tz=${encodeURIComponent(options.tz)}` : '';
119
- const r = await this.call('GET', `/api/my-crm/v1/rows/${encodeURIComponent(kind)}/${encodeURIComponent(id)}${qs}`);
123
+ const r = await this.request('GET', `/api/my-crm/v1/rows/${encodeURIComponent(kind)}/${encodeURIComponent(id)}${qs}`);
120
124
  return r.row;
121
125
  }
122
126
  catch (err) {
@@ -127,26 +131,67 @@ export class AwesomateClient {
127
131
  }
128
132
  /** The app kinds this account has defined. */
129
133
  async kinds() {
130
- return (await this.call('GET', '/api/my-crm/v1/kinds')).kinds;
134
+ return (await this.request('GET', '/api/my-crm/v1/kinds')).kinds;
131
135
  }
132
136
  /** Define an app kind (a table an app keeps). No migration: registry rows and generated views. */
133
137
  async defineKind(spec) {
134
- return (await this.call('POST', '/api/my-crm/v1/kinds', spec)).kind;
138
+ return (await this.request('POST', '/api/my-crm/v1/kinds', spec)).kind;
135
139
  }
136
140
  /** Create a record (returns its id), or change one with options.id. Every value is checked against the kind. */
137
141
  async write(kind, data, options = {}) {
138
- const r = await this.call('POST', '/api/my-crm/v1/call', {
142
+ const r = await this.request('POST', '/api/my-crm/v1/call', {
139
143
  fn: 'write_record', args: { kind, data, links: options.links ?? {}, ...(options.id ? { id: options.id } : {}) },
140
144
  }, false);
141
145
  return r.result.id;
142
146
  }
143
147
  /** Archive a record: it leaves every read and is kept for a restore. */
144
148
  async archive(kind, id) {
145
- await this.call('POST', '/api/my-crm/v1/call', { fn: 'archive_record', args: { kind, id } }, false);
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;
146
191
  }
147
192
  /** Grouped numbers (counts, sums) in the Business Data API's shape. */
148
193
  aggregate(request) {
149
- return this.call('POST', '/api/my-crm/v1/data/query', request);
194
+ return this.request('POST', '/api/my-crm/v1/data/query', request);
150
195
  }
151
196
  }
152
197
  export function createClient(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Read your own Awesomate data from Node: query and page through your contact list with types generated from your fields",
5
5
  "license": "MIT",
6
6
  "type": "module",