@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 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.1.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 call;
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.1.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 call(method, path, body, retry = true) {
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.call(method, path, body, false);
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.call('GET', '/api/my-crm/v1/rows/schema');
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.call('GET', '/api/my-crm/v1/rows/types');
98
+ return this.request('GET', '/api/my-crm/v1/rows/types');
94
99
  }
95
100
  async query(kind, options = {}) {
96
- 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 });
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.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}`);
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.call('POST', '/api/my-crm/v1/data/query', request);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.1.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",