js-bao 0.3.0 → 0.3.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/dist/browser.js CHANGED
@@ -1163,6 +1163,19 @@ var init_DocumentQueryTranslator = __esm({
1163
1163
 
1164
1164
  // src/models/metaSync.ts
1165
1165
  import * as Y from "yjs";
1166
+ function inferFieldType(value) {
1167
+ if (value instanceof Y.Map) return "stringset";
1168
+ switch (typeof value) {
1169
+ case "string":
1170
+ return "string";
1171
+ case "number":
1172
+ return "number";
1173
+ case "boolean":
1174
+ return "boolean";
1175
+ default:
1176
+ return null;
1177
+ }
1178
+ }
1166
1179
  function registerFunctionDefault(fn, name) {
1167
1180
  KNOWN_FUNCTION_DEFAULTS.set(fn, name);
1168
1181
  }
@@ -1174,6 +1187,11 @@ function encodeDefault(value) {
1174
1187
  }
1175
1188
  return value;
1176
1189
  }
1190
+ function clearMetaSyncCache(yDoc) {
1191
+ if (yDoc) {
1192
+ _syncedCache.delete(yDoc);
1193
+ }
1194
+ }
1177
1195
  function syncModelMeta(yDoc, modelName, schema) {
1178
1196
  let synced = _syncedCache.get(yDoc);
1179
1197
  if (synced?.has(modelName)) return;
@@ -1249,6 +1267,23 @@ function syncRelationshipMeta(relsMap, relName, relConfig) {
1249
1267
  }
1250
1268
  }
1251
1269
  }
1270
+ function syncInferredMeta(yDoc, modelName, recordData) {
1271
+ const meta = yDoc.getMap(`_meta_${modelName}`);
1272
+ for (const [fieldName, value] of Object.entries(recordData)) {
1273
+ if (fieldName.startsWith("_")) continue;
1274
+ let fieldMeta = meta.get(fieldName);
1275
+ if (!fieldMeta) {
1276
+ fieldMeta = new Y.Map();
1277
+ meta.set(fieldName, fieldMeta);
1278
+ }
1279
+ if (!fieldMeta.has("type")) {
1280
+ const inferredType = inferFieldType(value);
1281
+ if (inferredType) {
1282
+ fieldMeta.set("type", inferredType);
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1252
1287
  function setIfChanged(map, key, value) {
1253
1288
  if (map.get(key) !== value) {
1254
1289
  map.set(key, value);
@@ -6510,6 +6545,328 @@ function attachAndRegisterModel(modelClass, schema) {
6510
6545
  const runtimeShape = attachSchemaToClass(modelClass, schema);
6511
6546
  autoRegisterModel(modelClass, runtimeShape);
6512
6547
  }
6548
+
6549
+ // src/utils/yDocSchema.ts
6550
+ import * as Y3 from "yjs";
6551
+ function discoverSchema(yDoc) {
6552
+ const models = {};
6553
+ const metaNames = /* @__PURE__ */ new Set();
6554
+ for (const key of yDoc.share.keys()) {
6555
+ if (!key.startsWith("_meta_")) continue;
6556
+ const map = materializeMap(yDoc, key);
6557
+ if (!map) continue;
6558
+ const modelName = key.slice("_meta_".length);
6559
+ metaNames.add(modelName);
6560
+ models[modelName] = readModelMeta(map);
6561
+ }
6562
+ for (const key of yDoc.share.keys()) {
6563
+ if (key.startsWith("_")) continue;
6564
+ if (metaNames.has(key)) continue;
6565
+ const map = materializeMap(yDoc, key);
6566
+ if (!map || map.size === 0) continue;
6567
+ const inferred = inferModelFromData(map);
6568
+ if (inferred) models[key] = inferred;
6569
+ }
6570
+ return { models };
6571
+ }
6572
+ function discoverModelNames(yDoc) {
6573
+ const names = [];
6574
+ for (const key of yDoc.share.keys()) {
6575
+ if (key.startsWith("_")) continue;
6576
+ const map = materializeMap(yDoc, key);
6577
+ if (map) names.push(key);
6578
+ }
6579
+ return names.sort();
6580
+ }
6581
+ function materializeMap(yDoc, key) {
6582
+ try {
6583
+ const map = yDoc.getMap(key);
6584
+ return map instanceof Y3.Map ? map : null;
6585
+ } catch {
6586
+ return null;
6587
+ }
6588
+ }
6589
+ function readModelMeta(metaMap) {
6590
+ const fields = {};
6591
+ let constraints;
6592
+ let relationships;
6593
+ for (const [key, value] of metaMap.entries()) {
6594
+ if (key === "_constraints" && value instanceof Y3.Map) {
6595
+ constraints = readConstraints(value);
6596
+ } else if (key === "_relationships" && value instanceof Y3.Map) {
6597
+ relationships = readRelationships(value);
6598
+ } else if (value instanceof Y3.Map) {
6599
+ fields[key] = readFieldMeta(value);
6600
+ }
6601
+ }
6602
+ const model = { fields };
6603
+ if (constraints && Object.keys(constraints).length > 0) {
6604
+ model.constraints = constraints;
6605
+ }
6606
+ if (relationships && Object.keys(relationships).length > 0) {
6607
+ model.relationships = relationships;
6608
+ }
6609
+ return model;
6610
+ }
6611
+ function readFieldMeta(fieldMap) {
6612
+ const field = { type: fieldMap.get("type") ?? "unknown" };
6613
+ if (fieldMap.get("indexed") === true) field.indexed = true;
6614
+ if (fieldMap.get("unique") === true) field.unique = true;
6615
+ if (fieldMap.get("required") === true) field.required = true;
6616
+ if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
6617
+ const def = fieldMap.get("default");
6618
+ if (def !== void 0) field.default = def;
6619
+ const maxLength = fieldMap.get("maxLength");
6620
+ if (maxLength !== void 0) field.maxLength = maxLength;
6621
+ const maxCount = fieldMap.get("maxCount");
6622
+ if (maxCount !== void 0) field.maxCount = maxCount;
6623
+ return field;
6624
+ }
6625
+ function inferModelFromData(dataMap) {
6626
+ const fields = {};
6627
+ let sampled = 0;
6628
+ for (const [_recordId, recordValue] of dataMap.entries()) {
6629
+ if (!(recordValue instanceof Y3.Map)) continue;
6630
+ if (++sampled > 5) break;
6631
+ for (const [fieldName, value] of recordValue.entries()) {
6632
+ if (fieldName.startsWith("_")) continue;
6633
+ if (fields[fieldName]) continue;
6634
+ const type = inferTypeFromValue(value);
6635
+ if (type) fields[fieldName] = { type };
6636
+ }
6637
+ }
6638
+ if (Object.keys(fields).length === 0) return null;
6639
+ return { fields };
6640
+ }
6641
+ function inferTypeFromValue(value) {
6642
+ if (value instanceof Y3.Map) return "stringset";
6643
+ switch (typeof value) {
6644
+ case "string":
6645
+ return "string";
6646
+ case "number":
6647
+ return "number";
6648
+ case "boolean":
6649
+ return "boolean";
6650
+ default:
6651
+ return null;
6652
+ }
6653
+ }
6654
+ function readConstraints(constraintsMap) {
6655
+ const out = {};
6656
+ for (const [name, value] of constraintsMap.entries()) {
6657
+ if (!(value instanceof Y3.Map)) continue;
6658
+ let fields = [];
6659
+ const rawFields = value.get("fields");
6660
+ if (typeof rawFields === "string") {
6661
+ try {
6662
+ fields = JSON.parse(rawFields);
6663
+ } catch {
6664
+ }
6665
+ }
6666
+ out[name] = {
6667
+ type: value.get("type") ?? "unknown",
6668
+ fields
6669
+ };
6670
+ }
6671
+ return out;
6672
+ }
6673
+ function readRelationships(relsMap) {
6674
+ const out = {};
6675
+ for (const [name, value] of relsMap.entries()) {
6676
+ if (!(value instanceof Y3.Map)) continue;
6677
+ const rel = {};
6678
+ for (const [k, v] of value.entries()) {
6679
+ rel[k] = v;
6680
+ }
6681
+ out[name] = rel;
6682
+ }
6683
+ return out;
6684
+ }
6685
+ var CAMEL_TO_SNAKE = {
6686
+ autoAssign: "auto_assign",
6687
+ maxLength: "max_length",
6688
+ maxCount: "max_count",
6689
+ relatedIdField: "related_id_field",
6690
+ joinModel: "join_model",
6691
+ joinModelLocalField: "join_model_local_field",
6692
+ joinModelRelatedField: "join_model_related_field",
6693
+ joinModelOrderByField: "join_model_order_by_field",
6694
+ joinModelOrderDirection: "join_model_order_direction",
6695
+ orderByField: "order_by_field",
6696
+ orderDirection: "order_direction"
6697
+ };
6698
+ function toSnake(key) {
6699
+ return CAMEL_TO_SNAKE[key] ?? key;
6700
+ }
6701
+ function tomlValue(v) {
6702
+ if (typeof v === "string") {
6703
+ return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
6704
+ }
6705
+ return String(v);
6706
+ }
6707
+ function schemaToToml(schema) {
6708
+ const lines = [];
6709
+ for (const [modelName, model] of Object.entries(schema.models)) {
6710
+ if (lines.length > 0) lines.push("", "");
6711
+ lines.push(`[models.${modelName}]`);
6712
+ for (const [fieldName, field] of Object.entries(model.fields)) {
6713
+ lines.push("");
6714
+ lines.push(`[models.${modelName}.fields.${fieldName}]`);
6715
+ lines.push(`type = ${tomlValue(field.type)}`);
6716
+ if (field.autoAssign) lines.push("auto_assign = true");
6717
+ if (field.indexed) lines.push("indexed = true");
6718
+ if (field.unique) lines.push("unique = true");
6719
+ if (field.required) lines.push("required = true");
6720
+ if (field.maxLength !== void 0) lines.push(`max_length = ${field.maxLength}`);
6721
+ if (field.maxCount !== void 0) lines.push(`max_count = ${field.maxCount}`);
6722
+ if (field.default !== void 0) lines.push(`default = ${tomlValue(field.default)}`);
6723
+ }
6724
+ if (model.relationships) {
6725
+ for (const [relName, rel] of Object.entries(model.relationships)) {
6726
+ lines.push("");
6727
+ lines.push(`[models.${modelName}.relationships.${relName}]`);
6728
+ for (const [k, v] of Object.entries(rel)) {
6729
+ if (v === void 0) continue;
6730
+ lines.push(`${toSnake(k)} = ${tomlValue(v)}`);
6731
+ }
6732
+ }
6733
+ }
6734
+ if (model.constraints) {
6735
+ for (const [cName, c] of Object.entries(model.constraints)) {
6736
+ lines.push("");
6737
+ lines.push(`[[models.${modelName}.unique_constraints]]`);
6738
+ lines.push(`name = ${tomlValue(cName)}`);
6739
+ lines.push(`fields = [${c.fields.map((f) => tomlValue(f)).join(", ")}]`);
6740
+ }
6741
+ }
6742
+ }
6743
+ lines.push("");
6744
+ return lines.join("\n");
6745
+ }
6746
+
6747
+ // src/models/tomlLoader.ts
6748
+ import { parse as parseToml } from "smol-toml";
6749
+ var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
6750
+ "string",
6751
+ "number",
6752
+ "boolean",
6753
+ "date",
6754
+ "id",
6755
+ "stringset"
6756
+ ]);
6757
+ function parseFieldOptions(raw) {
6758
+ if (!raw.type || !VALID_FIELD_TYPES.has(raw.type)) {
6759
+ throw new Error(
6760
+ `Invalid field type "${raw.type}". Must be one of: ${[...VALID_FIELD_TYPES].join(", ")}`
6761
+ );
6762
+ }
6763
+ const opts = { type: raw.type };
6764
+ if (raw.indexed === true) opts.indexed = true;
6765
+ if (raw.unique === true) opts.unique = true;
6766
+ if (raw.required === true) opts.required = true;
6767
+ if (raw.auto_assign === true) opts.autoAssign = true;
6768
+ if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
6769
+ if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
6770
+ if (raw.default !== void 0) opts.default = raw.default;
6771
+ return opts;
6772
+ }
6773
+ function requireField(raw, field, context) {
6774
+ if (!raw[field]) {
6775
+ throw new Error(`Relationship ${context}: missing required field "${field}"`);
6776
+ }
6777
+ }
6778
+ function parseRelationship(raw) {
6779
+ const type = raw.type;
6780
+ if (type === "refersTo") {
6781
+ requireField(raw, "model", "refersTo");
6782
+ requireField(raw, "related_id_field", "refersTo");
6783
+ return {
6784
+ type: "refersTo",
6785
+ model: raw.model,
6786
+ relatedIdField: raw.related_id_field
6787
+ };
6788
+ }
6789
+ if (type === "hasMany") {
6790
+ requireField(raw, "model", "hasMany");
6791
+ requireField(raw, "related_id_field", "hasMany");
6792
+ const rel = {
6793
+ type: "hasMany",
6794
+ model: raw.model,
6795
+ relatedIdField: raw.related_id_field
6796
+ };
6797
+ if (raw.order_by_field) rel.orderByField = raw.order_by_field;
6798
+ if (raw.order_direction) rel.orderDirection = raw.order_direction;
6799
+ return rel;
6800
+ }
6801
+ if (type === "hasManyThrough") {
6802
+ requireField(raw, "model", "hasManyThrough");
6803
+ requireField(raw, "join_model", "hasManyThrough");
6804
+ requireField(raw, "join_model_local_field", "hasManyThrough");
6805
+ requireField(raw, "join_model_related_field", "hasManyThrough");
6806
+ const rel = {
6807
+ type: "hasManyThrough",
6808
+ model: raw.model,
6809
+ joinModel: raw.join_model,
6810
+ joinModelLocalField: raw.join_model_local_field,
6811
+ joinModelRelatedField: raw.join_model_related_field
6812
+ };
6813
+ if (raw.join_model_order_by_field)
6814
+ rel.joinModelOrderByField = raw.join_model_order_by_field;
6815
+ if (raw.join_model_order_direction)
6816
+ rel.joinModelOrderDirection = raw.join_model_order_direction;
6817
+ return rel;
6818
+ }
6819
+ throw new Error(`Unknown relationship type: ${type}`);
6820
+ }
6821
+ function loadSchemaFromTomlString(tomlString) {
6822
+ const parsed = parseToml(tomlString);
6823
+ const models = parsed.models;
6824
+ if (!models || typeof models !== "object") {
6825
+ throw new Error("TOML schema must have a [models] section");
6826
+ }
6827
+ const schemas = [];
6828
+ for (const [modelName, modelDef] of Object.entries(models)) {
6829
+ const fields = {};
6830
+ if (modelDef.fields) {
6831
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
6832
+ fields[fieldName] = parseFieldOptions(fieldDef);
6833
+ }
6834
+ }
6835
+ let relationships;
6836
+ if (modelDef.relationships) {
6837
+ relationships = {};
6838
+ for (const [relName, relDef] of Object.entries(
6839
+ modelDef.relationships
6840
+ )) {
6841
+ relationships[relName] = parseRelationship(relDef);
6842
+ }
6843
+ }
6844
+ let uniqueConstraints;
6845
+ if (modelDef.unique_constraints) {
6846
+ uniqueConstraints = [];
6847
+ for (const raw of modelDef.unique_constraints) {
6848
+ uniqueConstraints.push({
6849
+ name: raw.name,
6850
+ fields: [...raw.fields]
6851
+ });
6852
+ }
6853
+ }
6854
+ schemas.push(
6855
+ defineModelSchema({
6856
+ name: modelName,
6857
+ fields,
6858
+ options: {
6859
+ uniqueConstraints,
6860
+ relationships
6861
+ }
6862
+ })
6863
+ );
6864
+ }
6865
+ return schemas;
6866
+ }
6867
+
6868
+ // src/browser.ts
6869
+ init_metaSync();
6513
6870
  export {
6514
6871
  BaseModel2 as BaseModel,
6515
6872
  DatabaseEngine,
@@ -6526,13 +6883,22 @@ export {
6526
6883
  attachAndRegisterModel,
6527
6884
  attachSchemaToClass,
6528
6885
  autoRegisterModel,
6886
+ clearMetaSyncCache,
6529
6887
  createModelClass,
6530
6888
  defineModelSchema,
6531
6889
  detectEnvironment,
6890
+ discoverModelNames,
6891
+ discoverSchema,
6532
6892
  features,
6533
6893
  generateULID,
6894
+ inferFieldType,
6534
6895
  initJsBao,
6535
6896
  isBrowser,
6536
6897
  isNode,
6537
- resetJsBao
6898
+ loadSchemaFromTomlString,
6899
+ registerFunctionDefault,
6900
+ resetJsBao,
6901
+ schemaToToml,
6902
+ syncInferredMeta,
6903
+ syncModelMeta
6538
6904
  };
package/dist/client.cjs CHANGED
@@ -337,7 +337,8 @@ var DOClientEngine = class extends DatabaseEngine {
337
337
  data,
338
338
  stringSets,
339
339
  ifNotExists: options?.ifNotExists,
340
- condition: options?.condition
340
+ condition: options?.condition,
341
+ upsertOn: options?.upsertOn
341
342
  };
342
343
  const response = await this.doFetch("/save", request);
343
344
  return response.id;
@@ -670,22 +671,24 @@ function createModelAccessor(engine, modelName) {
670
671
  return result.data.length > 0 ? result.data[0] : null;
671
672
  },
672
673
  save: (data, options) => {
673
- if (!data.id) {
674
- throw new Error("Record must have an 'id' field");
675
- }
676
674
  let stringSets;
677
675
  let ifNotExists;
678
676
  let condition;
677
+ let upsertOn;
679
678
  if (options && typeof options === "object") {
680
- if (options.ifNotExists !== void 0 || options.stringSets !== void 0 || options.condition !== void 0) {
679
+ if (options.ifNotExists !== void 0 || options.stringSets !== void 0 || options.condition !== void 0 || options.upsertOn !== void 0) {
681
680
  stringSets = options.stringSets;
682
681
  ifNotExists = options.ifNotExists;
683
682
  condition = options.condition;
683
+ upsertOn = options.upsertOn;
684
684
  } else {
685
685
  stringSets = options;
686
686
  }
687
687
  }
688
- return engine.saveModel(modelName, data.id, data, stringSets, { ifNotExists, condition });
688
+ if (!data.id && !upsertOn) {
689
+ throw new Error("Record must have an 'id' field (or use upsertOn)");
690
+ }
691
+ return engine.saveModel(modelName, data.id, data, stringSets, { ifNotExists, condition, upsertOn });
689
692
  },
690
693
  patch: (id, data, options) => {
691
694
  let stringSets;
@@ -772,22 +775,24 @@ function connectDoDb(options) {
772
775
  return result.data.length > 0 ? result.data[0] : null;
773
776
  },
774
777
  save: (model, data, options2) => {
775
- if (!data.id) {
776
- throw new Error("Record must have an 'id' field");
777
- }
778
778
  let stringSets;
779
779
  let ifNotExists;
780
780
  let condition;
781
+ let upsertOn;
781
782
  if (options2 && typeof options2 === "object") {
782
- if (options2.ifNotExists !== void 0 || options2.stringSets !== void 0 || options2.condition !== void 0) {
783
+ if (options2.ifNotExists !== void 0 || options2.stringSets !== void 0 || options2.condition !== void 0 || options2.upsertOn !== void 0) {
783
784
  stringSets = options2.stringSets;
784
785
  ifNotExists = options2.ifNotExists;
785
786
  condition = options2.condition;
787
+ upsertOn = options2.upsertOn;
786
788
  } else {
787
789
  stringSets = options2;
788
790
  }
789
791
  }
790
- return engine.saveModel(resolveModelName(model), data.id, data, stringSets, { ifNotExists, condition });
792
+ if (!data.id && !upsertOn) {
793
+ throw new Error("Record must have an 'id' field (or use upsertOn)");
794
+ }
795
+ return engine.saveModel(resolveModelName(model), data.id, data, stringSets, { ifNotExists, condition, upsertOn });
791
796
  },
792
797
  patch: (model, id2, data, options2) => {
793
798
  let stringSets;
package/dist/client.d.cts CHANGED
@@ -825,9 +825,10 @@ declare class DOClientEngine extends DatabaseEngine {
825
825
  /**
826
826
  * Save a record to the DO.
827
827
  */
828
- saveModel(modelName: string, id: string, data: Record<string, any>, stringSets?: Record<string, string[]>, options?: {
828
+ saveModel(modelName: string, id: string | undefined, data: Record<string, any>, stringSets?: Record<string, string[]>, options?: {
829
829
  ifNotExists?: boolean;
830
830
  condition?: DocumentFilter;
831
+ upsertOn?: string;
831
832
  }): Promise<string>;
832
833
  /**
833
834
  * Patch (partial update) a record in the DO.
@@ -1017,6 +1018,7 @@ interface SaveOptions {
1017
1018
  stringSets?: Record<string, string[]>;
1018
1019
  ifNotExists?: boolean;
1019
1020
  condition?: DocumentFilter;
1021
+ upsertOn?: string;
1020
1022
  }
1021
1023
  interface PatchOptions {
1022
1024
  stringSets?: Record<string, string[]>;
package/dist/client.d.ts CHANGED
@@ -825,9 +825,10 @@ declare class DOClientEngine extends DatabaseEngine {
825
825
  /**
826
826
  * Save a record to the DO.
827
827
  */
828
- saveModel(modelName: string, id: string, data: Record<string, any>, stringSets?: Record<string, string[]>, options?: {
828
+ saveModel(modelName: string, id: string | undefined, data: Record<string, any>, stringSets?: Record<string, string[]>, options?: {
829
829
  ifNotExists?: boolean;
830
830
  condition?: DocumentFilter;
831
+ upsertOn?: string;
831
832
  }): Promise<string>;
832
833
  /**
833
834
  * Patch (partial update) a record in the DO.
@@ -1017,6 +1018,7 @@ interface SaveOptions {
1017
1018
  stringSets?: Record<string, string[]>;
1018
1019
  ifNotExists?: boolean;
1019
1020
  condition?: DocumentFilter;
1021
+ upsertOn?: string;
1020
1022
  }
1021
1023
  interface PatchOptions {
1022
1024
  stringSets?: Record<string, string[]>;
package/dist/client.js CHANGED
@@ -302,7 +302,8 @@ var DOClientEngine = class extends DatabaseEngine {
302
302
  data,
303
303
  stringSets,
304
304
  ifNotExists: options?.ifNotExists,
305
- condition: options?.condition
305
+ condition: options?.condition,
306
+ upsertOn: options?.upsertOn
306
307
  };
307
308
  const response = await this.doFetch("/save", request);
308
309
  return response.id;
@@ -635,22 +636,24 @@ function createModelAccessor(engine, modelName) {
635
636
  return result.data.length > 0 ? result.data[0] : null;
636
637
  },
637
638
  save: (data, options) => {
638
- if (!data.id) {
639
- throw new Error("Record must have an 'id' field");
640
- }
641
639
  let stringSets;
642
640
  let ifNotExists;
643
641
  let condition;
642
+ let upsertOn;
644
643
  if (options && typeof options === "object") {
645
- if (options.ifNotExists !== void 0 || options.stringSets !== void 0 || options.condition !== void 0) {
644
+ if (options.ifNotExists !== void 0 || options.stringSets !== void 0 || options.condition !== void 0 || options.upsertOn !== void 0) {
646
645
  stringSets = options.stringSets;
647
646
  ifNotExists = options.ifNotExists;
648
647
  condition = options.condition;
648
+ upsertOn = options.upsertOn;
649
649
  } else {
650
650
  stringSets = options;
651
651
  }
652
652
  }
653
- return engine.saveModel(modelName, data.id, data, stringSets, { ifNotExists, condition });
653
+ if (!data.id && !upsertOn) {
654
+ throw new Error("Record must have an 'id' field (or use upsertOn)");
655
+ }
656
+ return engine.saveModel(modelName, data.id, data, stringSets, { ifNotExists, condition, upsertOn });
654
657
  },
655
658
  patch: (id, data, options) => {
656
659
  let stringSets;
@@ -737,22 +740,24 @@ function connectDoDb(options) {
737
740
  return result.data.length > 0 ? result.data[0] : null;
738
741
  },
739
742
  save: (model, data, options2) => {
740
- if (!data.id) {
741
- throw new Error("Record must have an 'id' field");
742
- }
743
743
  let stringSets;
744
744
  let ifNotExists;
745
745
  let condition;
746
+ let upsertOn;
746
747
  if (options2 && typeof options2 === "object") {
747
- if (options2.ifNotExists !== void 0 || options2.stringSets !== void 0 || options2.condition !== void 0) {
748
+ if (options2.ifNotExists !== void 0 || options2.stringSets !== void 0 || options2.condition !== void 0 || options2.upsertOn !== void 0) {
748
749
  stringSets = options2.stringSets;
749
750
  ifNotExists = options2.ifNotExists;
750
751
  condition = options2.condition;
752
+ upsertOn = options2.upsertOn;
751
753
  } else {
752
754
  stringSets = options2;
753
755
  }
754
756
  }
755
- return engine.saveModel(resolveModelName(model), data.id, data, stringSets, { ifNotExists, condition });
757
+ if (!data.id && !upsertOn) {
758
+ throw new Error("Record must have an 'id' field (or use upsertOn)");
759
+ }
760
+ return engine.saveModel(resolveModelName(model), data.id, data, stringSets, { ifNotExists, condition, upsertOn });
756
761
  },
757
762
  patch: (model, id2, data, options2) => {
758
763
  let stringSets;