@liquidmetal-ai/drizzle 0.1.3 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidmetal-ai/drizzle",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Raindrop core operational libraries",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  import { expect, test } from 'vitest';
2
- import { buildManifest } from './build.js';
2
+ import { buildManifest, valueOf } from './build.js';
3
3
  import { Parser, Tokenizer } from './parse.js';
4
4
 
5
5
  const CONFIG = `
@@ -9,7 +9,7 @@ application "my-app" {
9
9
  service "my-service" {
10
10
  route {
11
11
  zone = "testymctestface.com"
12
- pattern = "testymctestface.com/*"
12
+ domain = "testymctestface.com"
13
13
  }
14
14
 
15
15
  domain {
@@ -36,6 +36,10 @@ application "my-app" {
36
36
  visibility = 'public'
37
37
  }
38
38
 
39
+ smartbucket "my-smart-bucket" {
40
+ location_hint = "enam"
41
+ }
42
+
39
43
  observer "my-observer" {
40
44
  env "API_KEY" {}
41
45
  source {
@@ -63,9 +67,15 @@ application "my-app" {
63
67
  sql_database "my-database" {}
64
68
 
65
69
  vector_index "my-index" {
66
- dimension = 768
70
+ dimensions = 768
67
71
  metric = 'cosine'
68
72
  }
73
+
74
+ task "my-task" {
75
+ type = "cron"
76
+ cron = "* * * * *"
77
+ visibility = 'protected'
78
+ }
69
79
  }
70
80
  `;
71
81
 
@@ -112,3 +122,75 @@ application "foo" {
112
122
  },
113
123
  ]);
114
124
  });
125
+
126
+ test('smartbucket parsing', () => {
127
+ const CONFIG = `
128
+ application "my-app" {
129
+ smartbucket "my-smart-bucket" {
130
+ location_hint = "enam"
131
+ }
132
+ }
133
+ `;
134
+ const tokenizer = new Tokenizer(CONFIG);
135
+ const parser = new Parser(tokenizer);
136
+ const ast = parser.parse();
137
+ expect(parser.errors).toEqual([]);
138
+
139
+ const [apps, errors] = buildManifest(ast);
140
+ expect(errors).toEqual([]);
141
+
142
+ // Check that we have exactly one application
143
+ expect(apps.length).toBe(1);
144
+
145
+ // Check that we have exactly one smartbucket
146
+ expect(apps[0]!.smartBucket.length).toBe(1);
147
+
148
+ // Check the smartbucket properties
149
+ const smartBucket = apps[0]!.smartBucket[0]!;
150
+ expect(valueOf(smartBucket.name)).toBe("my-smart-bucket");
151
+ expect(smartBucket.locationHint).toBeDefined();
152
+ expect(valueOf(smartBucket.locationHint!)).toBe("enam");
153
+ });
154
+
155
+ test('smartbucket with invalid properties', () => {
156
+ const CONFIG = `
157
+ application "my-app" {
158
+ smartbucket "my-smart-bucket" {
159
+ location_hint = "enam"
160
+ invalid_property = "should fail"
161
+ }
162
+ }
163
+ `;
164
+ const tokenizer = new Tokenizer(CONFIG);
165
+ const parser = new Parser(tokenizer);
166
+ const ast = parser.parse();
167
+ expect(parser.errors).toEqual([]);
168
+
169
+ const [, errors] = buildManifest(ast);
170
+
171
+ // Should have one error for the invalid property
172
+ expect(errors.length).toBe(1);
173
+ expect(errors[0]!.message).toContain('unexpected assignment');
174
+ });
175
+
176
+ test('smartbucket with duplicate assignment', () => {
177
+ const CONFIG = `
178
+ application "foo" {
179
+ smartbucket "my-smart-bucket" {
180
+ location_hint = "enam"
181
+ location_hint = "other-location"
182
+ }
183
+ }
184
+ `;
185
+ const tokenizer = new Tokenizer(CONFIG);
186
+ const parser = new Parser(tokenizer);
187
+ const ast = parser.parse();
188
+ expect(parser.errors).toEqual([]);
189
+
190
+ const [, errors] = buildManifest(ast);
191
+ expect(errors).toMatchObject([
192
+ {
193
+ message: 'duplicate location_hint assignment "other-location"',
194
+ },
195
+ ]);
196
+ });
@@ -25,7 +25,7 @@ export function valueOf(token: TokenString | TokenNumber | TokenBoolean): string
25
25
  if (token.type === 'string') {
26
26
  return token.value.slice(1, -1);
27
27
  }
28
- throw new Error(`unexpected token type`);
28
+ throw new Error(`unexpected token type: ${token}`);
29
29
  }
30
30
 
31
31
  export interface ConfigObject {
@@ -161,6 +161,9 @@ export function buildApplication(node: StanzaNode): [Application | undefined, Co
161
161
  case 'actor':
162
162
  buildStanza(buildActor, child, app.actor, errors);
163
163
  break;
164
+ case 'task':
165
+ buildStanza(buildTask, child, app.task, errors);
166
+ break;
164
167
  case 'bucket':
165
168
  buildStanza(buildBucket, child, app.bucket, errors);
166
169
  break;
@@ -173,8 +176,14 @@ export function buildApplication(node: StanzaNode): [Application | undefined, Co
173
176
  case 'sql_database':
174
177
  buildStanza(buildSqlDatabase, child, app.sqlDatabase, errors);
175
178
  break;
179
+ case 'kv_store':
180
+ buildStanza(buildKvStore, child, app.kvStore, errors);
181
+ break;
182
+ case 'smartbucket':
183
+ buildStanza(buildSmartBucket, child, app.smartBucket, errors);
184
+ break;
176
185
  default:
177
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
186
+ errors.push({ message: 'unexpected stanza', ...child });
178
187
  }
179
188
  break;
180
189
  case 'assignment':
@@ -215,7 +224,7 @@ function buildService(stanza: StanzaNode): [Service, ConfigError[]] {
215
224
  buildStanza(buildEnv, child, service.env, errors);
216
225
  break;
217
226
  default:
218
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
227
+ errors.push({ message: 'unexpected stanza', ...child });
219
228
  }
220
229
  break;
221
230
  case 'assignment':
@@ -245,13 +254,16 @@ function buildBucket(stanza: StanzaNode): [Bucket, ConfigError[]] {
245
254
  for (const child of stanza.block?.children ?? []) {
246
255
  switch (child.type) {
247
256
  case 'stanza':
248
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
257
+ errors.push({ message: 'unexpected stanza', ...child });
249
258
  break;
250
259
  case 'assignment':
251
260
  switch (child.key.value) {
252
261
  case 'visibility':
253
262
  buildAssignment(bucket, 'visibility', 'string', child, errors);
254
263
  break;
264
+ case 'location_hint':
265
+ buildAssignment(bucket, 'locationHint', 'string', child, errors);
266
+ break;
255
267
  default:
256
268
  errors.push({ message: 'unexpected assignment', ...child });
257
269
  }
@@ -274,7 +286,7 @@ function buildQueue(stanza: StanzaNode): [Queue, ConfigError[]] {
274
286
  for (const child of stanza.block?.children ?? []) {
275
287
  switch (child.type) {
276
288
  case 'stanza':
277
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
289
+ errors.push({ message: 'unexpected stanza', ...child });
278
290
  break;
279
291
  case 'assignment':
280
292
  switch (child.key.value) {
@@ -303,7 +315,7 @@ function buildVectorIndex(stanza: StanzaNode): [Bucket, ConfigError[]] {
303
315
  for (const child of stanza.block?.children ?? []) {
304
316
  switch (child.type) {
305
317
  case 'stanza':
306
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
318
+ errors.push({ message: 'unexpected stanza', ...child });
307
319
  break;
308
320
  case 'assignment':
309
321
  switch (child.key.value) {
@@ -338,7 +350,7 @@ function buildSqlDatabase(stanza: StanzaNode): [SqlDatabase, ConfigError[]] {
338
350
  for (const child of stanza.block?.children ?? []) {
339
351
  switch (child.type) {
340
352
  case 'stanza':
341
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
353
+ errors.push({ message: 'unexpected stanza', ...child });
342
354
  break;
343
355
  case 'assignment':
344
356
  switch (child.key.value) {
@@ -359,6 +371,62 @@ function buildSqlDatabase(stanza: StanzaNode): [SqlDatabase, ConfigError[]] {
359
371
  return [sqlDatabase, errors];
360
372
  }
361
373
 
374
+ function buildKvStore(stanza: StanzaNode): [KvStore, ConfigError[]] {
375
+ const errors: ConfigError[] = [];
376
+ const [name, nameErrors] = buildName1(stanza);
377
+ errors.push(...nameErrors);
378
+ const kvStore = new KvStore(name, stanza);
379
+
380
+ for (const child of stanza.block?.children ?? []) {
381
+ switch (child.type) {
382
+ case 'stanza':
383
+ errors.push({ message: 'unexpected stanza', ...child });
384
+ break;
385
+ case 'assignment':
386
+ switch (child.key.value) {
387
+ case 'visibility':
388
+ buildAssignment(kvStore, 'visibility', 'string', child, errors);
389
+ break;
390
+ default:
391
+ errors.push({ message: 'unexpected assignment', ...child });
392
+ }
393
+ break;
394
+ case 'comment':
395
+ case 'newline':
396
+ break;
397
+ default:
398
+ errors.push({ message: `unexpected ${child.type}`, ...child });
399
+ }
400
+ }
401
+ return [kvStore, errors];
402
+ }
403
+
404
+ function buildSmartBucket(stanza: StanzaNode): [SmartBucket, ConfigError[]] {
405
+ const errors: ConfigError[] = [];
406
+ const [name, nameErrors] = buildName1(stanza);
407
+ errors.push(...nameErrors);
408
+ const smartBucket = new SmartBucket(name, stanza);
409
+ for (const child of stanza.block?.children ?? []) {
410
+ switch (child.type) {
411
+ case 'assignment':
412
+ switch (child.key.value) {
413
+ case 'location_hint':
414
+ buildAssignment(smartBucket, 'locationHint', 'string', child, errors);
415
+ break;
416
+ default:
417
+ errors.push({ message: 'unexpected assignment', ...child });
418
+ }
419
+ break;
420
+ case 'comment':
421
+ case 'newline':
422
+ break;
423
+ default:
424
+ errors.push({ message: `unexpected ${child.type}`, ...child });
425
+ }
426
+ }
427
+ return [smartBucket, errors];
428
+ }
429
+
362
430
  function buildObserver(stanza: StanzaNode): [Observer, ConfigError[]] {
363
431
  const errors: ConfigError[] = [];
364
432
  const [name, nameErrors] = buildName1(stanza);
@@ -380,7 +448,7 @@ function buildObserver(stanza: StanzaNode): [Observer, ConfigError[]] {
380
448
  buildStanza(buildEnv, child, observer.env, errors);
381
449
  break;
382
450
  default:
383
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
451
+ errors.push({ message: 'unexpected stanza', ...child });
384
452
  }
385
453
  break;
386
454
  case 'assignment':
@@ -414,7 +482,7 @@ function buildActor(stanza: StanzaNode): [Actor, ConfigError[]] {
414
482
  buildStanza(buildEnv, child, actor.env, errors);
415
483
  break;
416
484
  default:
417
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
485
+ errors.push({ message: 'unexpected stanza', ...child });
418
486
  }
419
487
  break;
420
488
  case 'assignment':
@@ -437,6 +505,48 @@ function buildActor(stanza: StanzaNode): [Actor, ConfigError[]] {
437
505
  return [actor, errors];
438
506
  }
439
507
 
508
+ function buildTask(stanza: StanzaNode): [Task, ConfigError[]] {
509
+ const errors: ConfigError[] = [];
510
+ const [name, nameErrors] = buildName1(stanza);
511
+ errors.push(...nameErrors);
512
+ const task = new Task(name, stanza);
513
+ const children = stanza.block ? stanza.block.children : [];
514
+
515
+ for (const child of children) {
516
+ switch (child.type) {
517
+ case 'stanza':
518
+ switch (child.name) {
519
+ case 'binding':
520
+ buildStanza(buildBinding, child, task.bindings, errors);
521
+ break;
522
+ case 'env':
523
+ buildStanza(buildEnv, child, task.env, errors);
524
+ break;
525
+ default:
526
+ errors.push({ message: 'unexpected stanza', ...child });
527
+ }
528
+ break;
529
+ case 'assignment':
530
+ switch (child.key.value) {
531
+ case 'type':
532
+ buildAssignment(task, 'type', 'string', child, errors);
533
+ break;
534
+ case 'cron':
535
+ buildAssignment(task, 'cron', 'string', child, errors);
536
+ break;
537
+ case 'visibility':
538
+ buildAssignment(task, 'visibility', 'string', child, errors);
539
+ break;
540
+ default:
541
+ errors.push({ message: 'unexpected assignment', ...child });
542
+ }
543
+ break;
544
+ }
545
+ }
546
+
547
+ return [task, errors];
548
+ }
549
+
440
550
  function buildSource(stanza: StanzaNode): [Source, ConfigError[]] {
441
551
  const errors: ConfigError[] = [];
442
552
  const source = new Source(stanza);
@@ -486,13 +596,13 @@ function buildSource(stanza: StanzaNode): [Source, ConfigError[]] {
486
596
  buildAssignment(rule, 'suffix', 'string', ruleChild, errors);
487
597
  break;
488
598
  default:
489
- errors.push({ message: `unexpected assignment ${ruleChild.key}`, ...ruleChild });
599
+ errors.push({ message: 'unexpected assignment', ...ruleChild });
490
600
  }
491
601
  }
492
602
  }
493
603
  source.rule.push(rule);
494
604
  } else {
495
- errors.push({ message: `unexpected stanza ${child.name}`, ...child });
605
+ errors.push({ message: 'unexpected stanza', ...child });
496
606
  }
497
607
  }
498
608
  }
@@ -508,26 +618,26 @@ function buildRoute(stanza: StanzaNode): [Route | undefined, ConfigError[]] {
508
618
  for (const child of stanza.block?.children ?? []) {
509
619
  if (child.type === 'assignment') {
510
620
  switch (child.key.value) {
621
+ case 'cname':
622
+ buildAssignment(route, 'cname', 'string', child, errors);
623
+ break;
624
+ case 'domain':
625
+ buildAssignment(route, 'domain', 'string', child, errors);
626
+ break;
627
+ case 'path':
628
+ buildAssignment(route, 'path', 'string', child, errors);
629
+ break;
511
630
  case 'zone':
512
631
  buildAssignment(route, 'zone', 'string', child, errors);
513
632
  break;
514
- case 'pattern':
515
- buildAssignment(route, 'pattern', 'string', child, errors);
516
- break;
517
633
  default:
518
634
  errors.push({ message: `unexpected assignment ${child.key}`, ...child });
519
635
  }
520
636
  }
521
637
  }
522
- if (!route.zone) {
523
- errors.push({ message: 'missing zone assignment', ...stanza });
524
- return [undefined, errors];
525
- }
526
- if (!route.pattern) {
527
- errors.push({ message: 'missing pattern assignment', ...stanza });
528
- return [undefined, errors];
638
+ if (route.cname && route.domain) {
639
+ errors.push({ message: 'cname cannot be used with domain', ...stanza });
529
640
  }
530
- // TODO [ian]: validate zone and pattern contents.
531
641
  return [route, errors];
532
642
  }
533
643
 
@@ -605,11 +715,14 @@ export class Application {
605
715
  service: Service[] = [];
606
716
  observer: Observer[] = [];
607
717
  actor: Actor[] = [];
718
+ task: Task[] = [];
608
719
  bucket: Bucket[] = [];
609
720
  queue: Queue[] = [];
610
721
  env: Env[] = [];
611
722
  sqlDatabase: SqlDatabase[] = [];
612
723
  vectorIndex: VectorIndex[] = [];
724
+ kvStore: KvStore[] = [];
725
+ smartBucket: SmartBucket[] = [];
613
726
 
614
727
  constructor(name: TokenString, obj: ConfigObject) {
615
728
  this.name = name;
@@ -617,8 +730,8 @@ export class Application {
617
730
  }
618
731
 
619
732
  // Return all objects that require code handlers.
620
- handlers(): (Service | Observer | Actor)[] {
621
- return [...this.service, ...this.observer, ...this.actor];
733
+ handlers(): (Service | Observer | Actor | Task)[] {
734
+ return [...this.service, ...this.observer, ...this.actor, ...this.task];
622
735
  }
623
736
  }
624
737
 
@@ -650,6 +763,21 @@ export class Actor {
650
763
  }
651
764
  }
652
765
 
766
+ export class Task {
767
+ obj: ConfigObject;
768
+ name: TokenString;
769
+ type?: TokenString;
770
+ cron?: TokenString;
771
+ bindings: Binding[] = [];
772
+ env: Env[] = [];
773
+ visibility?: TokenString;
774
+
775
+ constructor(name: TokenString, obj: ConfigObject) {
776
+ this.name = name;
777
+ this.obj = obj;
778
+ }
779
+ }
780
+
653
781
  export class Observer {
654
782
  obj: ConfigObject;
655
783
  name: TokenString;
@@ -694,7 +822,9 @@ export class Rule {
694
822
  export class Route {
695
823
  obj: ConfigObject;
696
824
  zone?: TokenString;
697
- pattern?: TokenString;
825
+ domain?: TokenString;
826
+ cname?: TokenString;
827
+ path?: TokenString;
698
828
 
699
829
  constructor(obj: ConfigObject) {
700
830
  this.obj = obj;
@@ -734,6 +864,18 @@ export class Env {
734
864
  }
735
865
 
736
866
  export class Bucket {
867
+ name: TokenString;
868
+ visibility?: TokenString;
869
+ locationHint?: TokenString;
870
+ obj: ConfigObject;
871
+
872
+ constructor(name: TokenString, obj: ConfigObject) {
873
+ this.name = name;
874
+ this.obj = obj;
875
+ }
876
+ }
877
+
878
+ export class KvStore {
737
879
  name: TokenString;
738
880
  visibility?: TokenString;
739
881
  obj: ConfigObject;
@@ -780,5 +922,19 @@ export class SqlDatabase {
780
922
  }
781
923
  }
782
924
 
925
+ export class SmartBucket {
926
+ name: TokenString;
927
+ locationHint?: TokenString;
928
+ obj: ConfigObject;
929
+
930
+ constructor(name: TokenString, obj: ConfigObject) {
931
+ this.name = name;
932
+ this.obj = obj;
933
+ }
934
+ }
935
+
783
936
  export const VISIBILITIES = ['none', 'public', 'private', 'protected', 'application', 'suite', 'tenant'] as const;
784
937
  export type Visibility = (typeof VISIBILITIES)[number];
938
+
939
+ export const TASK_TYPES = ['cron'] as const;
940
+ export type TaskType = (typeof TASK_TYPES)[number];
@@ -1,4 +1,4 @@
1
- import { expect, test } from 'vitest';
1
+ import { expect, test, describe, it } from 'vitest';
2
2
  import { buildManifest } from './build.js';
3
3
  import { Parser, Tokenizer } from './parse.js';
4
4
  import { validate, VALIDATORS } from './validate.js';
@@ -11,6 +11,10 @@ application "my-app" {
11
11
  observer "my-observer" {}
12
12
  bucket "my-bucket" {}
13
13
  queue "my-queue" {}
14
+ task "my-cron" {
15
+ type = "cron"
16
+ cron = "* * * * *"
17
+ }
14
18
  }
15
19
  `;
16
20
  const tokenizer = new Tokenizer(manifest);
@@ -280,7 +284,7 @@ application "my-app" {
280
284
  const validateErrors = await validate(builtApps, VALIDATORS);
281
285
  expect(validateErrors).toMatchObject([
282
286
  {
283
- message: 'vector index must have a dimension',
287
+ message: 'vector index must have a dimensions',
284
288
  },
285
289
  {
286
290
  message: 'vector index metric must be either cosine or euclidean',
@@ -293,7 +297,7 @@ test('validate vector index parameters', async () => {
293
297
  application "my-app" {
294
298
  service "my-service" {}
295
299
  vector_index "my-index" {
296
- dimension = 128.9
300
+ dimensions = 128.9
297
301
  metric = "cosine"
298
302
  }
299
303
  }
@@ -305,7 +309,7 @@ application "my-app" {
305
309
  const validateErrors = await validate(builtApps, VALIDATORS);
306
310
  expect(validateErrors).toMatchObject([
307
311
  {
308
- message: 'vector index dimension must be an integer',
312
+ message: 'vector index dimensions must be an integer',
309
313
  },
310
314
  ]);
311
315
  });
@@ -433,3 +437,72 @@ application "my-app" {
433
437
  },
434
438
  ]);
435
439
  });
440
+
441
+ describe('task validator', async () => {
442
+ it('validates task types', async () => {
443
+ const manifest = `
444
+ application "my-app" {
445
+ task "my-task" {
446
+ type = "thingie" // not a valid task type
447
+ }
448
+ }
449
+ `;
450
+ const tokenizer = new Tokenizer(manifest);
451
+ const parser = new Parser(tokenizer);
452
+ const parsedManifest = parser.parse();
453
+ const [builtApps] = buildManifest(parsedManifest);
454
+ const validateErrors = await validate(builtApps, VALIDATORS);
455
+ expect(validateErrors).toMatchObject([
456
+ {
457
+ message: 'task type must be one of: cron',
458
+ },
459
+ ]);
460
+ });
461
+
462
+ it('validates task cron expressions', async () => {
463
+ const manifest = `
464
+ application "my-app" {
465
+ task "my-task" {
466
+ type = "cron"
467
+ cron = "* * * * */30"
468
+ }
469
+ }
470
+ `;
471
+ const tokenizer = new Tokenizer(manifest);
472
+ const parser = new Parser(tokenizer);
473
+ const parsedManifest = parser.parse();
474
+ const [builtApps] = buildManifest(parsedManifest);
475
+ const validateErrors = await validate(builtApps, VALIDATORS);
476
+ expect(validateErrors).toMatchObject([]);
477
+ });
478
+
479
+ it('invalidates task cron expressions', async () => {
480
+ const invalidManifests = [
481
+ `
482
+ application "my-app" {
483
+ task "my-task" {
484
+ type = "cron"
485
+ cron = "* * * *" // invalid, should have 5 parts
486
+ }
487
+ }
488
+ `,
489
+ `
490
+ application "my-app" {
491
+ task "my-task" {
492
+ type = "cron"
493
+ cron = "* * * * x" // invalid last part
494
+ }
495
+ }
496
+ `,
497
+ ];
498
+ for (const manifest of invalidManifests) {
499
+ const tokenizer = new Tokenizer(manifest);
500
+ const parser = new Parser(tokenizer);
501
+ const parsedManifest = parser.parse();
502
+ const [builtApps] = buildManifest(parsedManifest);
503
+ const validateErrors = await validate(builtApps, VALIDATORS);
504
+ expect(validateErrors.length).toBe(1);
505
+ expect(validateErrors[0]?.message).toContain('task cron expression is malformed');
506
+ }
507
+ });
508
+ });