rads-db 0.1.34 → 0.1.36

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
@@ -1,4 +1,11 @@
1
- # Introduction
1
+ ## Contributing guide
2
+
3
+ 1. Clone project
4
+ 2. `pnpm dev`
5
+ 3. Add tests
6
+ 4. Fix tests
7
+
8
+ ## Introduction
2
9
 
3
10
  When you work with the databases
4
11
 
package/dist/index.cjs CHANGED
@@ -46,8 +46,11 @@ function getZodSchema(zodSchemas, schema, key) {
46
46
  }
47
47
  const objectSchema = {};
48
48
  for (const fieldName in type.fields) {
49
- const shouldBeLazy = type.fields[fieldName].type === type.name;
50
49
  const f = type.fields[fieldName];
50
+ if (f.decorators?.computed || f.decorators?.precomputed) {
51
+ continue;
52
+ }
53
+ const shouldBeLazy = f.type === type.name;
51
54
  objectSchema[fieldName] = getFieldZodSchema(zodSchemas, schema, f, shouldBeLazy);
52
55
  }
53
56
  return zod.z.object(objectSchema);
@@ -83,7 +86,7 @@ function getFieldZodSchemaBase(zodSchemas, schema, field, shouldBeLazy) {
83
86
  if (schema[field.type]) {
84
87
  const getSchema = () => {
85
88
  let zodSchema = getZodSchema(zodSchemas, schema, field.type);
86
- if (schema[field.type].decorators.entity) {
89
+ if (field.isRelation) {
87
90
  zodSchema = zodSchema.deepPartial().required({ id: true });
88
91
  }
89
92
  return zodSchema;
@@ -305,7 +308,7 @@ const computedPresets = {
305
308
  }
306
309
  };
307
310
  async function handlePrecomputed(ctx, docs) {
308
- const { schema, typeName, options, drivers } = ctx;
311
+ const { schema, typeName, options, drivers, db } = ctx;
309
312
  if (drivers[typeName]?.driverName === "restApi")
310
313
  return;
311
314
  const { precomputedFields, nestedTypeFields, fields } = schema[typeName];
@@ -319,12 +322,12 @@ async function handlePrecomputed(ctx, docs) {
319
322
  throw new Error(`Computed handler for ${typeName}.${fieldName} was not found`);
320
323
  }
321
324
  const precomputedResults = docs.map(async ({ doc, oldDoc }) => {
322
- doc[fieldName] = await handler({ doc, oldDoc });
325
+ doc[fieldName] = await handler({ doc, oldDoc, db });
323
326
  });
324
327
  await Promise.all(precomputedResults);
325
328
  }
326
329
  for (const fieldName of nestedTypeFields || []) {
327
- const promises = docs.map(({ doc }) => {
330
+ const promises = docs.map(({ doc, db: db2 }) => {
328
331
  let subDoc = doc[fieldName];
329
332
  if (!subDoc)
330
333
  return void 0;
@@ -338,19 +341,20 @@ async function handlePrecomputed(ctx, docs) {
338
341
  await Promise.all(promises);
339
342
  }
340
343
  }
341
- async function handleComputed(schema, key, opts, data) {
342
- const { computedFields, nestedTypeFields, fields } = schema[key];
343
- const computed = opts.computed;
344
+ async function handleComputed(ctx, data) {
345
+ const { schema, typeName, options, db } = ctx;
346
+ const { computedFields, nestedTypeFields, fields } = schema[typeName];
347
+ const computed = options.computed;
344
348
  for (const fieldName of computedFields || []) {
345
- let handler = computed[key][fieldName];
349
+ let handler = computed[typeName][fieldName];
346
350
  if (!handler) {
347
351
  const preset = fields?.[fieldName].decorators?.computed?.preset;
348
352
  handler = computedPresets[preset || ""];
349
353
  if (!handler)
350
- throw new Error(`Computed handler for ${key}.${fieldName} was not found`);
354
+ throw new Error(`Computed handler for ${typeName}.${fieldName} was not found`);
351
355
  }
352
356
  const computedResults = data.map(async (doc) => {
353
- doc[fieldName] = await handler({ doc });
357
+ doc[fieldName] = await handler({ doc, db });
354
358
  });
355
359
  await Promise.all(computedResults);
356
360
  }
@@ -361,7 +365,7 @@ async function handleComputed(schema, key, opts, data) {
361
365
  return void 0;
362
366
  if (!___default.isArray(subDoc))
363
367
  subDoc = [subDoc];
364
- return handleComputed(schema, fields[fieldName].type, opts, subDoc);
368
+ return handleComputed({ ...ctx, typeName: fields[fieldName].type }, subDoc);
365
369
  });
366
370
  await Promise.all(promises);
367
371
  }
@@ -387,6 +391,16 @@ function verifyComputedPresense(schema, computed, effects) {
387
391
  }
388
392
  }
389
393
  }
394
+ async function handleEffectsBeforePut(ctx, docs) {
395
+ if (ctx.drivers[ctx.typeName]?.driverName === "restApi")
396
+ return [];
397
+ return Promise.all(ctx.effects[ctx.typeName].map((ef) => ef.beforePut?.(ctx, docs)));
398
+ }
399
+ async function handleEffectsAfterPut(ctx, docs, beforePutResults) {
400
+ if (ctx.drivers[ctx.typeName]?.driverName === "restApi")
401
+ return [];
402
+ return Promise.all(ctx.effects[ctx.typeName].map((ef, i) => ef.afterPut(ctx, docs, beforePutResults[i])));
403
+ }
390
404
 
391
405
  function diff(object, base, includeDenormFields = false) {
392
406
  if (!base)
@@ -498,34 +512,105 @@ function validateEventSourcingSetup(schema, entityName, eventEntityName, aggrega
498
512
  }
499
513
  }
500
514
  }
501
- async function handleEffectsBeforePut(ctx, docs) {
502
- if (ctx.drivers[ctx.typeName]?.driverName === "restApi")
503
- return [];
504
- return Promise.all(ctx.effects[ctx.typeName].map((ef) => ef.beforePut?.(ctx, docs)));
515
+
516
+ function verifyRelationsSetup(schema, effects) {
517
+ for (const entityName in schema) {
518
+ const entity = schema[entityName];
519
+ for (const fName in entity.fields) {
520
+ const f = entity.fields[fName];
521
+ const denormFields = f.relationDenormFields;
522
+ if (!f.isRelation || !denormFields)
523
+ continue;
524
+ effects[f.type].push({
525
+ async afterPut(ctx, docs, beforePutResult) {
526
+ const { drivers } = ctx;
527
+ const changedDocs = docs.filter((d) => denormFields.some((f2) => !___default.isEqual(d.doc[f2], d.oldDoc?.[f2])));
528
+ if (!changedDocs.length)
529
+ return;
530
+ const changedDocsById = ___default.keyBy(changedDocs, "doc.id");
531
+ const id_in = ___default.keys(changedDocsById);
532
+ if (!id_in.length)
533
+ return;
534
+ const affectedDocs = await drivers[entityName].getAll({
535
+ where: { [fName]: { id_in } },
536
+ maxItemCount: 1e3
537
+ });
538
+ if (!affectedDocs.length)
539
+ return;
540
+ for (const d of affectedDocs) {
541
+ const cd = changedDocsById[d[fName]?.id].doc;
542
+ if (!cd) {
543
+ console.warn(
544
+ `Error when updating denorm fields for ${entityName}.${fName}: cannot find updated ${f.type}#${d[fName]?.id}`
545
+ );
546
+ continue;
547
+ }
548
+ d[fName] = ___default.pick(cd, ["id", ...denormFields]);
549
+ }
550
+ await drivers[entityName].putMany(affectedDocs);
551
+ }
552
+ });
553
+ }
554
+ }
505
555
  }
506
- async function handleEffectsAfterPut(ctx, docs, beforePutResults) {
507
- if (ctx.drivers[ctx.typeName]?.driverName === "restApi")
508
- return [];
509
- return Promise.all(ctx.effects[ctx.typeName].map((ef, i) => ef.afterPut(ctx, docs, beforePutResults[i])));
556
+ async function fillDenormFieldsBeforePut(computedContext, docUpdates) {
557
+ const { schema, typeName, drivers } = computedContext;
558
+ const fields = schema[typeName].fields || {};
559
+ const relationsToInclude = ___default.values(fields).filter((f) => f.isRelation && f.relationDenormFields);
560
+ const docs = docUpdates.map((d) => d.doc);
561
+ const promises = relationsToInclude.map(async (f) => {
562
+ const docsWithRelation = docs.filter((d) => d[f.name]?.id);
563
+ const docsByRelationId = ___default.keyBy(docsWithRelation, `${f.name}.id`);
564
+ const id_in = ___default.keys(docsByRelationId);
565
+ if (!id_in)
566
+ return;
567
+ const driverInstance = drivers[f.type];
568
+ const relatedDocs = await driverInstance.getAll({ where: { id_in } });
569
+ const relatedDocsById = ___default.keyBy(relatedDocs, "id");
570
+ for (const d of docsWithRelation) {
571
+ const relatedDoc = relatedDocsById[d[f.name].id];
572
+ if (!relatedDoc) {
573
+ console.warn(`Relation ${f.type}#${d[f.name].id} not found for ${typeName}.${f.name}. Skipping...`);
574
+ }
575
+ d[f.name] = ___default.pick(relatedDoc, [...f.relationDenormFields, "id"]);
576
+ }
577
+ });
578
+ await Promise.all(promises);
510
579
  }
511
580
 
512
581
  function generateMethods(schema, validators, options) {
513
582
  const drivers = {};
514
- const db = { _schema: schema };
515
583
  const opts = { computed: {}, driver: memory(), ...options };
584
+ const db = {
585
+ _schema: schema,
586
+ uploadFile(args) {
587
+ if (!opts.fileUploadDriver)
588
+ throw new Error(`Missing configuration. Please specify "fileUploadDriver" argument in "createRads()".`);
589
+ return opts.fileUploadDriver.uploadFile(args);
590
+ }
591
+ };
516
592
  const effects = {};
517
593
  for (const key in schema) {
518
594
  effects[key] = [];
519
595
  }
520
596
  verifyComputedPresense(schema, opts.computed);
521
597
  verifyEventSourcingSetup(schema, effects);
598
+ verifyRelationsSetup(schema, effects);
522
599
  for (const key in schema) {
523
600
  if (!schema[key].decorators.entity)
524
601
  continue;
525
602
  const { handle } = schema[key];
526
603
  if (!handle)
527
604
  throw new Error(`Missing handle for entity ${key}`);
528
- const computedContext = { schema, typeName: key, validators, options: opts, drivers, effects };
605
+ const computedContext = {
606
+ schema,
607
+ typeName: key,
608
+ validators,
609
+ options: opts,
610
+ drivers,
611
+ effects,
612
+ db
613
+ };
529
614
  const driverInstance = getDriverInstance(schema, key, opts.driver, drivers);
530
615
  db[handle] = {
531
616
  getAgg: async (args, ctx) => {
@@ -540,7 +625,7 @@ function generateMethods(schema, validators, options) {
540
625
  if (result && args.include)
541
626
  await handleInclude(computedContext, args.include, [result]);
542
627
  if (result)
543
- await handleComputed(schema, key, opts, [result]);
628
+ await handleComputed(computedContext, [result]);
544
629
  return result;
545
630
  },
546
631
  getMany: async (args, ctx) => {
@@ -548,7 +633,7 @@ function generateMethods(schema, validators, options) {
548
633
  const result = await driverInstance.getMany(args, ctx);
549
634
  if (args.include)
550
635
  await handleInclude(computedContext, args.include, result.nodes);
551
- await handleComputed(schema, key, opts, result.nodes);
636
+ await handleComputed(computedContext, result.nodes);
552
637
  return result;
553
638
  },
554
639
  getAll: async (args, ctx) => {
@@ -556,7 +641,7 @@ function generateMethods(schema, validators, options) {
556
641
  const result = await driverInstance.getAll(args, ctx);
557
642
  if (args.include)
558
643
  await handleInclude(computedContext, args.include, result);
559
- await handleComputed(schema, key, opts, result);
644
+ await handleComputed(computedContext, result);
560
645
  return result;
561
646
  },
562
647
  put: async (doc, ctx) => {
@@ -568,10 +653,12 @@ function generateMethods(schema, validators, options) {
568
653
  if (oldDoc)
569
654
  doc = merge(oldDoc, doc);
570
655
  doc = validators[key](doc);
571
- await handlePrecomputed(computedContext, [{ doc, oldDoc }]);
572
- const beforePutResults = await handleEffectsBeforePut(computedContext, [{ doc, oldDoc }]);
656
+ const docArgsToSave = [{ doc, oldDoc }];
657
+ await fillDenormFieldsBeforePut(computedContext, docArgsToSave);
658
+ await handlePrecomputed(computedContext, docArgsToSave);
659
+ const beforePutResults = await handleEffectsBeforePut(computedContext, docArgsToSave);
573
660
  await driverInstance.put(doc, ctx);
574
- await handleEffectsAfterPut(computedContext, [{ doc, oldDoc }], beforePutResults);
661
+ await handleEffectsAfterPut(computedContext, docArgsToSave, beforePutResults);
575
662
  return doc;
576
663
  },
577
664
  putMany: async (docs, ctx) => {
@@ -590,6 +677,7 @@ function generateMethods(schema, validators, options) {
590
677
  doc = validators[key](doc);
591
678
  return { oldDoc, doc };
592
679
  });
680
+ await fillDenormFieldsBeforePut(computedContext, docArgsToSave);
593
681
  await handlePrecomputed(computedContext, docArgsToSave);
594
682
  const docsToSave = docArgsToSave.map((x) => x.doc);
595
683
  const beforePutResults = await handleEffectsBeforePut(computedContext, docArgsToSave);
@@ -606,7 +694,7 @@ async function handleInclude(computedContext, include, result) {
606
694
  return;
607
695
  const { schema, typeName, drivers } = computedContext;
608
696
  const fields = schema[typeName].fields || {};
609
- const relationsToInclude = ___default.keys(include).filter((key) => include[key] && schema[fields[key].type].decorators.entity);
697
+ const relationsToInclude = ___default.keys(include).filter((key) => include[key] && fields[key].isRelation);
610
698
  const downloadRelationsPromises = relationsToInclude.map(async (fieldName) => {
611
699
  const typeName2 = fields[fieldName].type;
612
700
  const type = schema[typeName2];
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@ type GetAggArgsAny = GetAggArgs<any, any>;
26
26
  type GetArgsInclude<E, EN extends keyof EntityMeta, R extends keyof E = keyof EntityMeta[EN]['relations'] & keyof E> = [R] extends [never] ? Record<string, never> : {
27
27
  _pick?: EntityMeta[EN]['primitives'][];
28
28
  } & {
29
- [K in R]?: GetArgsInclude<NonNullable<E[K]>, EntityMeta[EN]['relations'][K]>;
29
+ [K in R]?: GetArgsInclude<NonNullable<E[K]>, EntityMeta[EN]['relations'][K]['entityName']>;
30
30
  };
31
31
  type GetAggResponse<EN extends keyof EntityMeta, A extends GetAggArgs<EN, any>> = {
32
32
  [K in A['agg'][0]]: K extends '_count' ? number : number | undefined;
@@ -41,16 +41,19 @@ type GetResponse<E, EN extends keyof EntityMeta, A extends GetArgs<E, EN, any>>
41
41
  type GetResponseInclude<E, EN extends keyof EntityMeta, I extends GetArgsInclude<E, EN>> = I extends {
42
42
  _pick: string[];
43
43
  } ? GetResponseIncludeSelect<E, I> : {
44
- [K in keyof E]: K extends keyof EntityMeta[EN]['relations'] ? K extends keyof I ? GetResponseInclude<E[K], EntityMeta[EN]['relations'][K], I[K]> : Pick<E[K], 'id'> : E[K];
44
+ [K in keyof E]: K extends keyof EntityMeta[EN]['relations'] ? K extends keyof I ? GetResponseInclude<EntityMeta[EN]['relations'][K]['entity'], EntityMeta[EN]['relations'][K]['entityName'], I[K]> : Pick<EntityMeta[EN]['relations'][K]['entity'], EntityMeta[EN]['relations'][K]['denormFields']> : E[K];
45
45
  };
46
46
  interface GetResponseIncludeSelect<E, I> {
47
47
  }
48
48
  type GetResponseNoInclude<E, EN extends keyof EntityMeta> = {
49
- [K in keyof E]: K extends keyof EntityMeta[EN]['relations'] ? Pick<E[K], 'id'> : E[K];
49
+ [K in keyof E]: K extends keyof EntityMeta[EN]['relations'] ? Pick<EntityMeta[EN]['relations'][K]['entity'], EntityMeta[EN]['relations'][K]['denormFields']> : E[K];
50
50
  };
51
51
  type DeepPartial<T> = {
52
52
  [K in keyof T]?: DeepPartial<T[K]>;
53
53
  };
54
+ type Relation<T extends {
55
+ id: any;
56
+ }, K extends Exclude<keyof T, 'id'> = never> = Pick<T, K | 'id'> & DeepPartial<T>;
54
57
  type PutArgs<T> = {
55
58
  id: string;
56
59
  } & DeepPartial<T>;
@@ -114,6 +117,7 @@ interface CreateRadsArgs {
114
117
  schema?: Schema;
115
118
  driver?: DriverConstructor;
116
119
  drivers?: Record<string, DriverConstructor>;
120
+ fileUploadDriver?: FileUploadDriver;
117
121
  computed?: Record<string, Record<string, Function>>;
118
122
  context?: Record<string, any>;
119
123
  }
@@ -131,6 +135,16 @@ interface TypeDefinition {
131
135
  handle?: string;
132
136
  handlePlural?: string;
133
137
  }
138
+ interface FileUploadDriver {
139
+ driverName: string;
140
+ uploadFile(args: FileUploadArgs): Promise<string>;
141
+ }
142
+ interface FileUploadArgs {
143
+ blob: Blob;
144
+ containerName?: string;
145
+ fileName?: string;
146
+ options: any;
147
+ }
134
148
  interface MinimalDriver {
135
149
  driverName: string;
136
150
  putMany: (item: Record<string, any>[], ctx?: RadsRequestContext) => MaybePromise<void>;
@@ -152,10 +166,13 @@ interface FieldDefinition {
152
166
  defaultValueCopyFrom?: string;
153
167
  isRequired?: boolean;
154
168
  isArray?: boolean;
169
+ isRelation?: boolean;
170
+ relationDenormFields?: string[];
155
171
  comment?: string;
156
172
  decorators?: Record<string, Record<string, any>>;
157
173
  }
158
174
  interface ComputedContext {
175
+ db: RadsDb;
159
176
  schema: Schema;
160
177
  typeName: string;
161
178
  validators: SchemaValidators;
@@ -184,6 +201,13 @@ interface RestDriverOptions {
184
201
  }
185
202
  interface MemoryDriverOptions {
186
203
  }
204
+ interface MemoryFileUploadDriverOptions {
205
+ }
206
+ interface AzureStorageBlobUploadDriverOptions {
207
+ connectionString: string;
208
+ defaultContainer?: string;
209
+ fileNamePrefix?: string;
210
+ }
187
211
  interface AzureCosmosDriverOptions {
188
212
  endpoint: string;
189
213
  accountKey?: string;
@@ -211,4 +235,4 @@ declare function computed(meta?: ComputedDecoratorArgs): (a: any, b?: ClassField
211
235
  declare function createRads(args?: CreateRadsArgs): RadsDb;
212
236
  declare function getRestRoutes(db: RadsDb, prefix?: string): Record<string, Record<string, Function>>;
213
237
 
214
- export { AzureCosmosDriverOptions, Change, ComputedContext, ComputedDecoratorArgs, CreateRadsArgs, DeepPartial, Driver, DriverConstructor, DriverOptions, EntityDecoratorArgs, EntityMethods, FieldDecoratorArgs, FieldDefinition, GenerateClientNormalizedOptions, GenerateClientOptions, GetAggArgs, GetAggArgsAgg, GetAggArgsAny, GetAggResponse, GetArgs, GetArgsAny, GetArgsInclude, GetManyArgs, GetManyArgsAny, GetManyResponse, GetResponse, GetResponseInclude, GetResponseIncludeSelect, GetResponseNoInclude, MemoryDriverOptions, MinimalDriver, PutArgs, PutEffect, RadsRequestContext, RestDriverOptions, Schema, SchemaValidators, TypeDefinition, UiDecoratorArgs, UiFieldDecoratorArgs, computed, createRads, entity, field, getRestRoutes, precomputed, ui };
238
+ export { AzureCosmosDriverOptions, AzureStorageBlobUploadDriverOptions, Change, ComputedContext, ComputedDecoratorArgs, CreateRadsArgs, DeepPartial, Driver, DriverConstructor, DriverOptions, EntityDecoratorArgs, EntityMethods, FieldDecoratorArgs, FieldDefinition, FileUploadArgs, FileUploadDriver, GenerateClientNormalizedOptions, GenerateClientOptions, GetAggArgs, GetAggArgsAgg, GetAggArgsAny, GetAggResponse, GetArgs, GetArgsAny, GetArgsInclude, GetManyArgs, GetManyArgsAny, GetManyResponse, GetResponse, GetResponseInclude, GetResponseIncludeSelect, GetResponseNoInclude, MemoryDriverOptions, MemoryFileUploadDriverOptions, MinimalDriver, PutArgs, PutEffect, RadsRequestContext, Relation, RestDriverOptions, Schema, SchemaValidators, TypeDefinition, UiDecoratorArgs, UiFieldDecoratorArgs, computed, createRads, entity, field, getRestRoutes, precomputed, ui };
package/dist/index.mjs CHANGED
@@ -40,8 +40,11 @@ function getZodSchema(zodSchemas, schema, key) {
40
40
  }
41
41
  const objectSchema = {};
42
42
  for (const fieldName in type.fields) {
43
- const shouldBeLazy = type.fields[fieldName].type === type.name;
44
43
  const f = type.fields[fieldName];
44
+ if (f.decorators?.computed || f.decorators?.precomputed) {
45
+ continue;
46
+ }
47
+ const shouldBeLazy = f.type === type.name;
45
48
  objectSchema[fieldName] = getFieldZodSchema(zodSchemas, schema, f, shouldBeLazy);
46
49
  }
47
50
  return z.object(objectSchema);
@@ -77,7 +80,7 @@ function getFieldZodSchemaBase(zodSchemas, schema, field, shouldBeLazy) {
77
80
  if (schema[field.type]) {
78
81
  const getSchema = () => {
79
82
  let zodSchema = getZodSchema(zodSchemas, schema, field.type);
80
- if (schema[field.type].decorators.entity) {
83
+ if (field.isRelation) {
81
84
  zodSchema = zodSchema.deepPartial().required({ id: true });
82
85
  }
83
86
  return zodSchema;
@@ -299,7 +302,7 @@ const computedPresets = {
299
302
  }
300
303
  };
301
304
  async function handlePrecomputed(ctx, docs) {
302
- const { schema, typeName, options, drivers } = ctx;
305
+ const { schema, typeName, options, drivers, db } = ctx;
303
306
  if (drivers[typeName]?.driverName === "restApi")
304
307
  return;
305
308
  const { precomputedFields, nestedTypeFields, fields } = schema[typeName];
@@ -313,12 +316,12 @@ async function handlePrecomputed(ctx, docs) {
313
316
  throw new Error(`Computed handler for ${typeName}.${fieldName} was not found`);
314
317
  }
315
318
  const precomputedResults = docs.map(async ({ doc, oldDoc }) => {
316
- doc[fieldName] = await handler({ doc, oldDoc });
319
+ doc[fieldName] = await handler({ doc, oldDoc, db });
317
320
  });
318
321
  await Promise.all(precomputedResults);
319
322
  }
320
323
  for (const fieldName of nestedTypeFields || []) {
321
- const promises = docs.map(({ doc }) => {
324
+ const promises = docs.map(({ doc, db: db2 }) => {
322
325
  let subDoc = doc[fieldName];
323
326
  if (!subDoc)
324
327
  return void 0;
@@ -332,19 +335,20 @@ async function handlePrecomputed(ctx, docs) {
332
335
  await Promise.all(promises);
333
336
  }
334
337
  }
335
- async function handleComputed(schema, key, opts, data) {
336
- const { computedFields, nestedTypeFields, fields } = schema[key];
337
- const computed = opts.computed;
338
+ async function handleComputed(ctx, data) {
339
+ const { schema, typeName, options, db } = ctx;
340
+ const { computedFields, nestedTypeFields, fields } = schema[typeName];
341
+ const computed = options.computed;
338
342
  for (const fieldName of computedFields || []) {
339
- let handler = computed[key][fieldName];
343
+ let handler = computed[typeName][fieldName];
340
344
  if (!handler) {
341
345
  const preset = fields?.[fieldName].decorators?.computed?.preset;
342
346
  handler = computedPresets[preset || ""];
343
347
  if (!handler)
344
- throw new Error(`Computed handler for ${key}.${fieldName} was not found`);
348
+ throw new Error(`Computed handler for ${typeName}.${fieldName} was not found`);
345
349
  }
346
350
  const computedResults = data.map(async (doc) => {
347
- doc[fieldName] = await handler({ doc });
351
+ doc[fieldName] = await handler({ doc, db });
348
352
  });
349
353
  await Promise.all(computedResults);
350
354
  }
@@ -355,7 +359,7 @@ async function handleComputed(schema, key, opts, data) {
355
359
  return void 0;
356
360
  if (!_.isArray(subDoc))
357
361
  subDoc = [subDoc];
358
- return handleComputed(schema, fields[fieldName].type, opts, subDoc);
362
+ return handleComputed({ ...ctx, typeName: fields[fieldName].type }, subDoc);
359
363
  });
360
364
  await Promise.all(promises);
361
365
  }
@@ -381,6 +385,16 @@ function verifyComputedPresense(schema, computed, effects) {
381
385
  }
382
386
  }
383
387
  }
388
+ async function handleEffectsBeforePut(ctx, docs) {
389
+ if (ctx.drivers[ctx.typeName]?.driverName === "restApi")
390
+ return [];
391
+ return Promise.all(ctx.effects[ctx.typeName].map((ef) => ef.beforePut?.(ctx, docs)));
392
+ }
393
+ async function handleEffectsAfterPut(ctx, docs, beforePutResults) {
394
+ if (ctx.drivers[ctx.typeName]?.driverName === "restApi")
395
+ return [];
396
+ return Promise.all(ctx.effects[ctx.typeName].map((ef, i) => ef.afterPut(ctx, docs, beforePutResults[i])));
397
+ }
384
398
 
385
399
  function diff(object, base, includeDenormFields = false) {
386
400
  if (!base)
@@ -492,34 +506,105 @@ function validateEventSourcingSetup(schema, entityName, eventEntityName, aggrega
492
506
  }
493
507
  }
494
508
  }
495
- async function handleEffectsBeforePut(ctx, docs) {
496
- if (ctx.drivers[ctx.typeName]?.driverName === "restApi")
497
- return [];
498
- return Promise.all(ctx.effects[ctx.typeName].map((ef) => ef.beforePut?.(ctx, docs)));
509
+
510
+ function verifyRelationsSetup(schema, effects) {
511
+ for (const entityName in schema) {
512
+ const entity = schema[entityName];
513
+ for (const fName in entity.fields) {
514
+ const f = entity.fields[fName];
515
+ const denormFields = f.relationDenormFields;
516
+ if (!f.isRelation || !denormFields)
517
+ continue;
518
+ effects[f.type].push({
519
+ async afterPut(ctx, docs, beforePutResult) {
520
+ const { drivers } = ctx;
521
+ const changedDocs = docs.filter((d) => denormFields.some((f2) => !_.isEqual(d.doc[f2], d.oldDoc?.[f2])));
522
+ if (!changedDocs.length)
523
+ return;
524
+ const changedDocsById = _.keyBy(changedDocs, "doc.id");
525
+ const id_in = _.keys(changedDocsById);
526
+ if (!id_in.length)
527
+ return;
528
+ const affectedDocs = await drivers[entityName].getAll({
529
+ where: { [fName]: { id_in } },
530
+ maxItemCount: 1e3
531
+ });
532
+ if (!affectedDocs.length)
533
+ return;
534
+ for (const d of affectedDocs) {
535
+ const cd = changedDocsById[d[fName]?.id].doc;
536
+ if (!cd) {
537
+ console.warn(
538
+ `Error when updating denorm fields for ${entityName}.${fName}: cannot find updated ${f.type}#${d[fName]?.id}`
539
+ );
540
+ continue;
541
+ }
542
+ d[fName] = _.pick(cd, ["id", ...denormFields]);
543
+ }
544
+ await drivers[entityName].putMany(affectedDocs);
545
+ }
546
+ });
547
+ }
548
+ }
499
549
  }
500
- async function handleEffectsAfterPut(ctx, docs, beforePutResults) {
501
- if (ctx.drivers[ctx.typeName]?.driverName === "restApi")
502
- return [];
503
- return Promise.all(ctx.effects[ctx.typeName].map((ef, i) => ef.afterPut(ctx, docs, beforePutResults[i])));
550
+ async function fillDenormFieldsBeforePut(computedContext, docUpdates) {
551
+ const { schema, typeName, drivers } = computedContext;
552
+ const fields = schema[typeName].fields || {};
553
+ const relationsToInclude = _.values(fields).filter((f) => f.isRelation && f.relationDenormFields);
554
+ const docs = docUpdates.map((d) => d.doc);
555
+ const promises = relationsToInclude.map(async (f) => {
556
+ const docsWithRelation = docs.filter((d) => d[f.name]?.id);
557
+ const docsByRelationId = _.keyBy(docsWithRelation, `${f.name}.id`);
558
+ const id_in = _.keys(docsByRelationId);
559
+ if (!id_in)
560
+ return;
561
+ const driverInstance = drivers[f.type];
562
+ const relatedDocs = await driverInstance.getAll({ where: { id_in } });
563
+ const relatedDocsById = _.keyBy(relatedDocs, "id");
564
+ for (const d of docsWithRelation) {
565
+ const relatedDoc = relatedDocsById[d[f.name].id];
566
+ if (!relatedDoc) {
567
+ console.warn(`Relation ${f.type}#${d[f.name].id} not found for ${typeName}.${f.name}. Skipping...`);
568
+ }
569
+ d[f.name] = _.pick(relatedDoc, [...f.relationDenormFields, "id"]);
570
+ }
571
+ });
572
+ await Promise.all(promises);
504
573
  }
505
574
 
506
575
  function generateMethods(schema, validators, options) {
507
576
  const drivers = {};
508
- const db = { _schema: schema };
509
577
  const opts = { computed: {}, driver: memory(), ...options };
578
+ const db = {
579
+ _schema: schema,
580
+ uploadFile(args) {
581
+ if (!opts.fileUploadDriver)
582
+ throw new Error(`Missing configuration. Please specify "fileUploadDriver" argument in "createRads()".`);
583
+ return opts.fileUploadDriver.uploadFile(args);
584
+ }
585
+ };
510
586
  const effects = {};
511
587
  for (const key in schema) {
512
588
  effects[key] = [];
513
589
  }
514
590
  verifyComputedPresense(schema, opts.computed);
515
591
  verifyEventSourcingSetup(schema, effects);
592
+ verifyRelationsSetup(schema, effects);
516
593
  for (const key in schema) {
517
594
  if (!schema[key].decorators.entity)
518
595
  continue;
519
596
  const { handle } = schema[key];
520
597
  if (!handle)
521
598
  throw new Error(`Missing handle for entity ${key}`);
522
- const computedContext = { schema, typeName: key, validators, options: opts, drivers, effects };
599
+ const computedContext = {
600
+ schema,
601
+ typeName: key,
602
+ validators,
603
+ options: opts,
604
+ drivers,
605
+ effects,
606
+ db
607
+ };
523
608
  const driverInstance = getDriverInstance(schema, key, opts.driver, drivers);
524
609
  db[handle] = {
525
610
  getAgg: async (args, ctx) => {
@@ -534,7 +619,7 @@ function generateMethods(schema, validators, options) {
534
619
  if (result && args.include)
535
620
  await handleInclude(computedContext, args.include, [result]);
536
621
  if (result)
537
- await handleComputed(schema, key, opts, [result]);
622
+ await handleComputed(computedContext, [result]);
538
623
  return result;
539
624
  },
540
625
  getMany: async (args, ctx) => {
@@ -542,7 +627,7 @@ function generateMethods(schema, validators, options) {
542
627
  const result = await driverInstance.getMany(args, ctx);
543
628
  if (args.include)
544
629
  await handleInclude(computedContext, args.include, result.nodes);
545
- await handleComputed(schema, key, opts, result.nodes);
630
+ await handleComputed(computedContext, result.nodes);
546
631
  return result;
547
632
  },
548
633
  getAll: async (args, ctx) => {
@@ -550,7 +635,7 @@ function generateMethods(schema, validators, options) {
550
635
  const result = await driverInstance.getAll(args, ctx);
551
636
  if (args.include)
552
637
  await handleInclude(computedContext, args.include, result);
553
- await handleComputed(schema, key, opts, result);
638
+ await handleComputed(computedContext, result);
554
639
  return result;
555
640
  },
556
641
  put: async (doc, ctx) => {
@@ -562,10 +647,12 @@ function generateMethods(schema, validators, options) {
562
647
  if (oldDoc)
563
648
  doc = merge(oldDoc, doc);
564
649
  doc = validators[key](doc);
565
- await handlePrecomputed(computedContext, [{ doc, oldDoc }]);
566
- const beforePutResults = await handleEffectsBeforePut(computedContext, [{ doc, oldDoc }]);
650
+ const docArgsToSave = [{ doc, oldDoc }];
651
+ await fillDenormFieldsBeforePut(computedContext, docArgsToSave);
652
+ await handlePrecomputed(computedContext, docArgsToSave);
653
+ const beforePutResults = await handleEffectsBeforePut(computedContext, docArgsToSave);
567
654
  await driverInstance.put(doc, ctx);
568
- await handleEffectsAfterPut(computedContext, [{ doc, oldDoc }], beforePutResults);
655
+ await handleEffectsAfterPut(computedContext, docArgsToSave, beforePutResults);
569
656
  return doc;
570
657
  },
571
658
  putMany: async (docs, ctx) => {
@@ -584,6 +671,7 @@ function generateMethods(schema, validators, options) {
584
671
  doc = validators[key](doc);
585
672
  return { oldDoc, doc };
586
673
  });
674
+ await fillDenormFieldsBeforePut(computedContext, docArgsToSave);
587
675
  await handlePrecomputed(computedContext, docArgsToSave);
588
676
  const docsToSave = docArgsToSave.map((x) => x.doc);
589
677
  const beforePutResults = await handleEffectsBeforePut(computedContext, docArgsToSave);
@@ -600,7 +688,7 @@ async function handleInclude(computedContext, include, result) {
600
688
  return;
601
689
  const { schema, typeName, drivers } = computedContext;
602
690
  const fields = schema[typeName].fields || {};
603
- const relationsToInclude = _.keys(include).filter((key) => include[key] && schema[fields[key].type].decorators.entity);
691
+ const relationsToInclude = _.keys(include).filter((key) => include[key] && fields[key].isRelation);
604
692
  const downloadRelationsPromises = relationsToInclude.map(async (fieldName) => {
605
693
  const typeName2 = fields[fieldName].type;
606
694
  const type = schema[typeName2];
@@ -63,6 +63,7 @@ function parseSchema(typescriptFiles) {
63
63
  }
64
64
  fillComputedDefinitionsForType(result);
65
65
  processThisReferenceDefaultValues(result, typesMap);
66
+ verifyRelationFields(result);
66
67
  return result;
67
68
  }
68
69
  function processThisReferenceDefaultValues(result, typesMap) {
@@ -205,6 +206,8 @@ function parseClassMember(node, parentName, ctx) {
205
206
  const decorators = parseDecorators(node.modifiers, ctx);
206
207
  const {
207
208
  isArray,
209
+ isRelation,
210
+ relationDenormFields,
208
211
  type
209
212
  } = parseFieldType(ctx, parentName, name, node, defaultValueDescription);
210
213
  const result = {
@@ -214,13 +217,17 @@ function parseClassMember(node, parentName, ctx) {
214
217
  name,
215
218
  isRequired,
216
219
  isArray,
220
+ isRelation,
221
+ relationDenormFields,
217
222
  comment
218
223
  };
219
224
  if (!_lodash.default.isEmpty(decorators)) result.decorators = decorators;
220
225
  return result;
221
226
  }
222
227
  function parseFieldType(ctx, parentName, fieldName, node, defaultValueDescription) {
223
- let isArray = false;
228
+ let isArray;
229
+ let isRelation;
230
+ let relationDenormFields;
224
231
  let nodeType = node?.type;
225
232
  if (nodeType?.kind === _typescript.SyntaxKind.ArrayType) {
226
233
  isArray = true;
@@ -232,6 +239,18 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueDescriptio
232
239
  throw new Error(`Nested arrays are not supported (${parentName})`);
233
240
  }
234
241
  }
242
+ if (nodeType?.kind === _typescript.SyntaxKind.TypeReference) {
243
+ const nt = nodeType;
244
+ if (nt.typeName.getText(ctx.sourceFile) === "Relation") {
245
+ if (!nt.typeArguments?.length) throw new Error(`Missing type argument for Relation<>: '${parentName}'`);
246
+ nodeType = nt.typeArguments[0];
247
+ isRelation = true;
248
+ if (nt.typeArguments[1]) {
249
+ const ta = nt.typeArguments[1];
250
+ relationDenormFields = getRelationDenormFields(ctx, ta);
251
+ }
252
+ }
253
+ }
235
254
  if (nodeType?.kind === _typescript.SyntaxKind.UnionType) {
236
255
  const newTypeName = `${parentName}_${_lodash.default.upperFirst(fieldName)}`;
237
256
  ctx.result[newTypeName] = {
@@ -241,6 +260,8 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueDescriptio
241
260
  };
242
261
  return {
243
262
  isArray,
263
+ isRelation,
264
+ relationDenormFields,
244
265
  type: newTypeName
245
266
  };
246
267
  }
@@ -259,9 +280,25 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueDescriptio
259
280
  }
260
281
  return {
261
282
  isArray,
283
+ isRelation,
284
+ relationDenormFields,
262
285
  type
263
286
  };
264
287
  }
288
+ function getRelationDenormFields(ctx, node) {
289
+ if (node.kind === _typescript.SyntaxKind.LiteralType) {
290
+ const literal = node.literal;
291
+ if (literal.kind === _typescript.SyntaxKind.StringLiteral) {
292
+ return [literal.text];
293
+ }
294
+ throw new Error(`Unexpected type - ${literal.getText(ctx.sourceFile)}`);
295
+ }
296
+ if (node.kind === _typescript.SyntaxKind.UnionType) {
297
+ const union = node;
298
+ return union.types.flatMap(t => getRelationDenormFields(ctx, t));
299
+ }
300
+ throw new Error(`Unexpected type - ${node.getText(ctx.sourceFile)}`);
301
+ }
265
302
  function verifyDefaultValueType(isArray, defaultValueDescription, type, supportedPrimitiveTypes2, ctx) {
266
303
  if (isArray) {
267
304
  if (defaultValueDescription.type !== "array") {
@@ -357,4 +394,15 @@ function getClassDeclarations(sourceFile) {
357
394
  const syntaxListNode = children.find(c => c.kind === _typescript.SyntaxKind.SyntaxList);
358
395
  const result = syntaxListNode?.getChildren().filter(c => [_typescript.SyntaxKind.ClassDeclaration, _typescript.SyntaxKind.TypeAliasDeclaration].includes(c.kind));
359
396
  return result || [];
397
+ }
398
+ function verifyRelationFields(result) {
399
+ for (const key in result) {
400
+ const type = result[key];
401
+ for (const fieldName in type.fields) {
402
+ const field = type.fields[fieldName];
403
+ if (field.isRelation && !result[field.type]?.decorators?.entity) {
404
+ throw new Error(`${key}.${fieldName}: Relation must point to an entity. Got "${field.type}" instead.`);
405
+ }
406
+ }
407
+ }
360
408
  }
@@ -43,6 +43,7 @@ export function parseSchema(typescriptFiles) {
43
43
  }
44
44
  fillComputedDefinitionsForType(result);
45
45
  processThisReferenceDefaultValues(result, typesMap);
46
+ verifyRelationFields(result);
46
47
  return result;
47
48
  }
48
49
  function processThisReferenceDefaultValues(result, typesMap) {
@@ -181,14 +182,32 @@ function parseClassMember(node, parentName, ctx) {
181
182
  const isRequired = !node.questionToken;
182
183
  const comment = node.jsDoc?.[0]?.comment;
183
184
  const decorators = parseDecorators(node.modifiers, ctx);
184
- const { isArray, type } = parseFieldType(ctx, parentName, name, node, defaultValueDescription);
185
- const result = { type, defaultValue, defaultValueCopyFrom, name, isRequired, isArray, comment };
185
+ const { isArray, isRelation, relationDenormFields, type } = parseFieldType(
186
+ ctx,
187
+ parentName,
188
+ name,
189
+ node,
190
+ defaultValueDescription
191
+ );
192
+ const result = {
193
+ type,
194
+ defaultValue,
195
+ defaultValueCopyFrom,
196
+ name,
197
+ isRequired,
198
+ isArray,
199
+ isRelation,
200
+ relationDenormFields,
201
+ comment
202
+ };
186
203
  if (!_.isEmpty(decorators))
187
204
  result.decorators = decorators;
188
205
  return result;
189
206
  }
190
207
  function parseFieldType(ctx, parentName, fieldName, node, defaultValueDescription) {
191
- let isArray = false;
208
+ let isArray;
209
+ let isRelation;
210
+ let relationDenormFields;
192
211
  let nodeType = node?.type;
193
212
  if (nodeType?.kind === SyntaxKind.ArrayType) {
194
213
  isArray = true;
@@ -200,6 +219,19 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueDescriptio
200
219
  throw new Error(`Nested arrays are not supported (${parentName})`);
201
220
  }
202
221
  }
222
+ if (nodeType?.kind === SyntaxKind.TypeReference) {
223
+ const nt = nodeType;
224
+ if (nt.typeName.getText(ctx.sourceFile) === "Relation") {
225
+ if (!nt.typeArguments?.length)
226
+ throw new Error(`Missing type argument for Relation<>: '${parentName}'`);
227
+ nodeType = nt.typeArguments[0];
228
+ isRelation = true;
229
+ if (nt.typeArguments[1]) {
230
+ const ta = nt.typeArguments[1];
231
+ relationDenormFields = getRelationDenormFields(ctx, ta);
232
+ }
233
+ }
234
+ }
203
235
  if (nodeType?.kind === SyntaxKind.UnionType) {
204
236
  const newTypeName = `${parentName}_${_.upperFirst(fieldName)}`;
205
237
  ctx.result[newTypeName] = {
@@ -207,7 +239,7 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueDescriptio
207
239
  decorators: {},
208
240
  enumValues: getEnumValues(nodeType, node, ctx)
209
241
  };
210
- return { isArray, type: newTypeName };
242
+ return { isArray, isRelation, relationDenormFields, type: newTypeName };
211
243
  }
212
244
  const type = nodeType?.getText(ctx.sourceFile) ?? defaultValueDescription?.type;
213
245
  if (!type)
@@ -224,7 +256,21 @@ function parseFieldType(ctx, parentName, fieldName, node, defaultValueDescriptio
224
256
  if (defaultValueDescription && defaultValueDescription.type !== "thisReference") {
225
257
  verifyDefaultValueType(isArray, defaultValueDescription, type, supportedPrimitiveTypes, ctx);
226
258
  }
227
- return { isArray, type };
259
+ return { isArray, isRelation, relationDenormFields, type };
260
+ }
261
+ function getRelationDenormFields(ctx, node) {
262
+ if (node.kind === SyntaxKind.LiteralType) {
263
+ const literal = node.literal;
264
+ if (literal.kind === SyntaxKind.StringLiteral) {
265
+ return [literal.text];
266
+ }
267
+ throw new Error(`Unexpected type - ${literal.getText(ctx.sourceFile)}`);
268
+ }
269
+ if (node.kind === SyntaxKind.UnionType) {
270
+ const union = node;
271
+ return union.types.flatMap((t) => getRelationDenormFields(ctx, t));
272
+ }
273
+ throw new Error(`Unexpected type - ${node.getText(ctx.sourceFile)}`);
228
274
  }
229
275
  function verifyDefaultValueType(isArray, defaultValueDescription, type, supportedPrimitiveTypes2, ctx) {
230
276
  if (isArray) {
@@ -308,3 +354,14 @@ function getClassDeclarations(sourceFile) {
308
354
  const result = syntaxListNode?.getChildren().filter((c) => [SyntaxKind.ClassDeclaration, SyntaxKind.TypeAliasDeclaration].includes(c.kind));
309
355
  return result || [];
310
356
  }
357
+ function verifyRelationFields(result) {
358
+ for (const key in result) {
359
+ const type = result[key];
360
+ for (const fieldName in type.fields) {
361
+ const field = type.fields[fieldName];
362
+ if (field.isRelation && !result[field.type]?.decorators?.entity) {
363
+ throw new Error(`${key}.${fieldName}: Relation must point to an entity. Got "${field.type}" instead.`);
364
+ }
365
+ }
366
+ }
367
+ }
@@ -69,9 +69,13 @@ function getIndexDts(schema, options) {
69
69
  imports.push(`import type { ${type.name} } from '../../${options.entitiesDir}/${key}'`);
70
70
  rootFields.push(`${type.handle}: EntityMethods<${type.name}, '${type.name}', ${type.name}_Where>`);
71
71
  const fieldsArray = Object.values(type.fields);
72
+ const relations = _lodash.default.fromPairs(fieldsArray.filter(f => f.isRelation).map(f => [f.name, {
73
+ entity: f.type,
74
+ denormFields: [...(f.relationDenormFields || []), "id"].map(x => `'${x}'`).join(" | ")
75
+ }]));
72
76
  entityMeta.push({
73
77
  type: type.name,
74
- relations: _lodash.default.fromPairs(fieldsArray.filter(f => schema[f.type]?.decorators?.entity).map(x => [x.name, x.type])),
78
+ relationsStr: _lodash.default.keys(relations).map(k => `${k}: { entityName: '${relations[k].entity}', entity: ${relations[k].entity}, denormFields: ${relations[k].denormFields} }`),
75
79
  aggregates: fieldsArray.filter(f => f.type === "number" && !f.isArray).map(x => `'${x.name}'`).join(" | "),
76
80
  primitives: fieldsArray.filter(f => !schema[f.type]?.decorators?.entity).map(x => `'${x.name}'`).join(" | ")
77
81
  });
@@ -86,11 +90,11 @@ ${whereFields.join("\n")}
86
90
  type: ${x.type}
87
91
  primitives: ${x.primitives || "never"}
88
92
  aggregates: ${x.aggregates || "never"}
89
- relations: ${JSON.stringify(x.relations)}
93
+ relations: {${x.relationsStr}}
90
94
  }
91
95
  `.trim()).join("\n");
92
96
  return `
93
- import type { EntityMethods } from 'rads-db'
97
+ import type { EntityMethods, FileUploadArgs } from 'rads-db'
94
98
  ${imports.join("\n")}
95
99
 
96
100
  export interface EntityMeta {
@@ -106,6 +110,7 @@ ${whereTypes.join("\n\n")}
106
110
 
107
111
  export interface RadsDb {
108
112
  _schema: any
113
+ uploadFile: (args: FileUploadArgs) => Promise<string>
109
114
  ${rootFields.join("\n")}
110
115
  }
111
116
  `.trim();
@@ -68,9 +68,17 @@ function getIndexDts(schema, options) {
68
68
  imports.push(`import type { ${type.name} } from '../../${options.entitiesDir}/${key}'`);
69
69
  rootFields.push(`${type.handle}: EntityMethods<${type.name}, '${type.name}', ${type.name}_Where>`);
70
70
  const fieldsArray = Object.values(type.fields);
71
+ const relations = _.fromPairs(
72
+ fieldsArray.filter((f) => f.isRelation).map((f) => [
73
+ f.name,
74
+ { entity: f.type, denormFields: [...f.relationDenormFields || [], "id"].map((x) => `'${x}'`).join(" | ") }
75
+ ])
76
+ );
71
77
  entityMeta.push({
72
78
  type: type.name,
73
- relations: _.fromPairs(fieldsArray.filter((f) => schema[f.type]?.decorators?.entity).map((x) => [x.name, x.type])),
79
+ relationsStr: _.keys(relations).map(
80
+ (k) => `${k}: { entityName: '${relations[k].entity}', entity: ${relations[k].entity}, denormFields: ${relations[k].denormFields} }`
81
+ ),
74
82
  aggregates: fieldsArray.filter((f) => f.type === "number" && !f.isArray).map((x) => `'${x.name}'`).join(" | "),
75
83
  primitives: fieldsArray.filter((f) => !schema[f.type]?.decorators?.entity).map((x) => `'${x.name}'`).join(" | ")
76
84
  });
@@ -88,12 +96,12 @@ ${whereFields.join("\n")}
88
96
  type: ${x.type}
89
97
  primitives: ${x.primitives || "never"}
90
98
  aggregates: ${x.aggregates || "never"}
91
- relations: ${JSON.stringify(x.relations)}
99
+ relations: {${x.relationsStr}}
92
100
  }
93
101
  `.trim()
94
102
  ).join("\n");
95
103
  return `
96
- import type { EntityMethods } from 'rads-db'
104
+ import type { EntityMethods, FileUploadArgs } from 'rads-db'
97
105
  ${imports.join("\n")}
98
106
 
99
107
  export interface EntityMeta {
@@ -109,6 +117,7 @@ ${whereTypes.join("\n\n")}
109
117
 
110
118
  export interface RadsDb {
111
119
  _schema: any
120
+ uploadFile: (args: FileUploadArgs) => Promise<string>
112
121
  ${rootFields.join("\n")}
113
122
  }
114
123
  `.trim();
package/package.json CHANGED
@@ -28,17 +28,21 @@
28
28
  "require": "./integrations/*.cjs"
29
29
  }
30
30
  },
31
- "version": "0.1.34",
31
+ "version": "0.1.36",
32
32
  "description": "Say goodbye to boilerplate code and hello to efficient and elegant syntax.",
33
33
  "keywords": [],
34
34
  "author": "",
35
35
  "license": "ISC",
36
36
  "peerDependencies": {
37
+ "@azure/storage-blob": ">=12",
37
38
  "@azure/cosmos": ">=3",
38
39
  "h3": "*",
39
40
  "typescript": ">=5.0.0"
40
41
  },
41
42
  "peerDependenciesMeta": {
43
+ "@azure/storage-blob": {
44
+ "optional": true
45
+ },
42
46
  "@azure/cosmos": {
43
47
  "optional": true
44
48
  },
@@ -72,11 +76,10 @@
72
76
  "zod": "^3.21.4"
73
77
  },
74
78
  "scripts": {
75
- "predev": "pnpm link-rads-db",
76
79
  "test": "vitest --run && vitest typecheck --run",
77
80
  "link-rads-db": "symlink-dir ./test/_rads-db ./node_modules/_rads-db",
78
81
  "generate-test-schema": "rads-db",
79
- "dev": "pnpm install --frozen-lockfile && vitest --ui",
82
+ "dev": "pnpm install --frozen-lockfile && pnpm build && pnpm generate-test-schema && pnpm link-rads-db && vitest --ui",
80
83
  "lint": "cross-env NODE_ENV=production eslint src/**/*.* test/**/*.*",
81
84
  "dev-typecheck": "vitest typecheck --ui",
82
85
  "build": "unbuild"