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/node.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);
@@ -7127,17 +7162,344 @@ function attachAndRegisterModel(modelClass, schema) {
7127
7162
  autoRegisterModel(modelClass, runtimeShape);
7128
7163
  }
7129
7164
 
7130
- // src/utils/yDocDump.ts
7165
+ // src/utils/yDocSchema.ts
7131
7166
  import * as Y3 from "yjs";
7167
+ function discoverSchema(yDoc) {
7168
+ const models = {};
7169
+ const metaNames = /* @__PURE__ */ new Set();
7170
+ for (const key of yDoc.share.keys()) {
7171
+ if (!key.startsWith("_meta_")) continue;
7172
+ const map = materializeMap(yDoc, key);
7173
+ if (!map) continue;
7174
+ const modelName = key.slice("_meta_".length);
7175
+ metaNames.add(modelName);
7176
+ models[modelName] = readModelMeta(map);
7177
+ }
7178
+ for (const key of yDoc.share.keys()) {
7179
+ if (key.startsWith("_")) continue;
7180
+ if (metaNames.has(key)) continue;
7181
+ const map = materializeMap(yDoc, key);
7182
+ if (!map || map.size === 0) continue;
7183
+ const inferred = inferModelFromData(map);
7184
+ if (inferred) models[key] = inferred;
7185
+ }
7186
+ return { models };
7187
+ }
7188
+ function discoverModelNames(yDoc) {
7189
+ const names = [];
7190
+ for (const key of yDoc.share.keys()) {
7191
+ if (key.startsWith("_")) continue;
7192
+ const map = materializeMap(yDoc, key);
7193
+ if (map) names.push(key);
7194
+ }
7195
+ return names.sort();
7196
+ }
7197
+ function materializeMap(yDoc, key) {
7198
+ try {
7199
+ const map = yDoc.getMap(key);
7200
+ return map instanceof Y3.Map ? map : null;
7201
+ } catch {
7202
+ return null;
7203
+ }
7204
+ }
7205
+ function readModelMeta(metaMap) {
7206
+ const fields = {};
7207
+ let constraints;
7208
+ let relationships;
7209
+ for (const [key, value] of metaMap.entries()) {
7210
+ if (key === "_constraints" && value instanceof Y3.Map) {
7211
+ constraints = readConstraints(value);
7212
+ } else if (key === "_relationships" && value instanceof Y3.Map) {
7213
+ relationships = readRelationships(value);
7214
+ } else if (value instanceof Y3.Map) {
7215
+ fields[key] = readFieldMeta(value);
7216
+ }
7217
+ }
7218
+ const model = { fields };
7219
+ if (constraints && Object.keys(constraints).length > 0) {
7220
+ model.constraints = constraints;
7221
+ }
7222
+ if (relationships && Object.keys(relationships).length > 0) {
7223
+ model.relationships = relationships;
7224
+ }
7225
+ return model;
7226
+ }
7227
+ function readFieldMeta(fieldMap) {
7228
+ const field = { type: fieldMap.get("type") ?? "unknown" };
7229
+ if (fieldMap.get("indexed") === true) field.indexed = true;
7230
+ if (fieldMap.get("unique") === true) field.unique = true;
7231
+ if (fieldMap.get("required") === true) field.required = true;
7232
+ if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7233
+ const def = fieldMap.get("default");
7234
+ if (def !== void 0) field.default = def;
7235
+ const maxLength = fieldMap.get("maxLength");
7236
+ if (maxLength !== void 0) field.maxLength = maxLength;
7237
+ const maxCount = fieldMap.get("maxCount");
7238
+ if (maxCount !== void 0) field.maxCount = maxCount;
7239
+ return field;
7240
+ }
7241
+ function inferModelFromData(dataMap) {
7242
+ const fields = {};
7243
+ let sampled = 0;
7244
+ for (const [_recordId, recordValue] of dataMap.entries()) {
7245
+ if (!(recordValue instanceof Y3.Map)) continue;
7246
+ if (++sampled > 5) break;
7247
+ for (const [fieldName, value] of recordValue.entries()) {
7248
+ if (fieldName.startsWith("_")) continue;
7249
+ if (fields[fieldName]) continue;
7250
+ const type = inferTypeFromValue(value);
7251
+ if (type) fields[fieldName] = { type };
7252
+ }
7253
+ }
7254
+ if (Object.keys(fields).length === 0) return null;
7255
+ return { fields };
7256
+ }
7257
+ function inferTypeFromValue(value) {
7258
+ if (value instanceof Y3.Map) return "stringset";
7259
+ switch (typeof value) {
7260
+ case "string":
7261
+ return "string";
7262
+ case "number":
7263
+ return "number";
7264
+ case "boolean":
7265
+ return "boolean";
7266
+ default:
7267
+ return null;
7268
+ }
7269
+ }
7270
+ function readConstraints(constraintsMap) {
7271
+ const out = {};
7272
+ for (const [name, value] of constraintsMap.entries()) {
7273
+ if (!(value instanceof Y3.Map)) continue;
7274
+ let fields = [];
7275
+ const rawFields = value.get("fields");
7276
+ if (typeof rawFields === "string") {
7277
+ try {
7278
+ fields = JSON.parse(rawFields);
7279
+ } catch {
7280
+ }
7281
+ }
7282
+ out[name] = {
7283
+ type: value.get("type") ?? "unknown",
7284
+ fields
7285
+ };
7286
+ }
7287
+ return out;
7288
+ }
7289
+ function readRelationships(relsMap) {
7290
+ const out = {};
7291
+ for (const [name, value] of relsMap.entries()) {
7292
+ if (!(value instanceof Y3.Map)) continue;
7293
+ const rel = {};
7294
+ for (const [k, v] of value.entries()) {
7295
+ rel[k] = v;
7296
+ }
7297
+ out[name] = rel;
7298
+ }
7299
+ return out;
7300
+ }
7301
+ var CAMEL_TO_SNAKE = {
7302
+ autoAssign: "auto_assign",
7303
+ maxLength: "max_length",
7304
+ maxCount: "max_count",
7305
+ relatedIdField: "related_id_field",
7306
+ joinModel: "join_model",
7307
+ joinModelLocalField: "join_model_local_field",
7308
+ joinModelRelatedField: "join_model_related_field",
7309
+ joinModelOrderByField: "join_model_order_by_field",
7310
+ joinModelOrderDirection: "join_model_order_direction",
7311
+ orderByField: "order_by_field",
7312
+ orderDirection: "order_direction"
7313
+ };
7314
+ function toSnake(key) {
7315
+ return CAMEL_TO_SNAKE[key] ?? key;
7316
+ }
7317
+ function tomlValue(v) {
7318
+ if (typeof v === "string") {
7319
+ return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
7320
+ }
7321
+ return String(v);
7322
+ }
7323
+ function schemaToToml(schema) {
7324
+ const lines = [];
7325
+ for (const [modelName, model] of Object.entries(schema.models)) {
7326
+ if (lines.length > 0) lines.push("", "");
7327
+ lines.push(`[models.${modelName}]`);
7328
+ for (const [fieldName, field] of Object.entries(model.fields)) {
7329
+ lines.push("");
7330
+ lines.push(`[models.${modelName}.fields.${fieldName}]`);
7331
+ lines.push(`type = ${tomlValue(field.type)}`);
7332
+ if (field.autoAssign) lines.push("auto_assign = true");
7333
+ if (field.indexed) lines.push("indexed = true");
7334
+ if (field.unique) lines.push("unique = true");
7335
+ if (field.required) lines.push("required = true");
7336
+ if (field.maxLength !== void 0) lines.push(`max_length = ${field.maxLength}`);
7337
+ if (field.maxCount !== void 0) lines.push(`max_count = ${field.maxCount}`);
7338
+ if (field.default !== void 0) lines.push(`default = ${tomlValue(field.default)}`);
7339
+ }
7340
+ if (model.relationships) {
7341
+ for (const [relName, rel] of Object.entries(model.relationships)) {
7342
+ lines.push("");
7343
+ lines.push(`[models.${modelName}.relationships.${relName}]`);
7344
+ for (const [k, v] of Object.entries(rel)) {
7345
+ if (v === void 0) continue;
7346
+ lines.push(`${toSnake(k)} = ${tomlValue(v)}`);
7347
+ }
7348
+ }
7349
+ }
7350
+ if (model.constraints) {
7351
+ for (const [cName, c] of Object.entries(model.constraints)) {
7352
+ lines.push("");
7353
+ lines.push(`[[models.${modelName}.unique_constraints]]`);
7354
+ lines.push(`name = ${tomlValue(cName)}`);
7355
+ lines.push(`fields = [${c.fields.map((f) => tomlValue(f)).join(", ")}]`);
7356
+ }
7357
+ }
7358
+ }
7359
+ lines.push("");
7360
+ return lines.join("\n");
7361
+ }
7362
+
7363
+ // src/models/tomlLoader.ts
7364
+ import { parse as parseToml } from "smol-toml";
7365
+ var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
7366
+ "string",
7367
+ "number",
7368
+ "boolean",
7369
+ "date",
7370
+ "id",
7371
+ "stringset"
7372
+ ]);
7373
+ function parseFieldOptions(raw) {
7374
+ if (!raw.type || !VALID_FIELD_TYPES.has(raw.type)) {
7375
+ throw new Error(
7376
+ `Invalid field type "${raw.type}". Must be one of: ${[...VALID_FIELD_TYPES].join(", ")}`
7377
+ );
7378
+ }
7379
+ const opts = { type: raw.type };
7380
+ if (raw.indexed === true) opts.indexed = true;
7381
+ if (raw.unique === true) opts.unique = true;
7382
+ if (raw.required === true) opts.required = true;
7383
+ if (raw.auto_assign === true) opts.autoAssign = true;
7384
+ if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7385
+ if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7386
+ if (raw.default !== void 0) opts.default = raw.default;
7387
+ return opts;
7388
+ }
7389
+ function requireField(raw, field, context) {
7390
+ if (!raw[field]) {
7391
+ throw new Error(`Relationship ${context}: missing required field "${field}"`);
7392
+ }
7393
+ }
7394
+ function parseRelationship(raw) {
7395
+ const type = raw.type;
7396
+ if (type === "refersTo") {
7397
+ requireField(raw, "model", "refersTo");
7398
+ requireField(raw, "related_id_field", "refersTo");
7399
+ return {
7400
+ type: "refersTo",
7401
+ model: raw.model,
7402
+ relatedIdField: raw.related_id_field
7403
+ };
7404
+ }
7405
+ if (type === "hasMany") {
7406
+ requireField(raw, "model", "hasMany");
7407
+ requireField(raw, "related_id_field", "hasMany");
7408
+ const rel = {
7409
+ type: "hasMany",
7410
+ model: raw.model,
7411
+ relatedIdField: raw.related_id_field
7412
+ };
7413
+ if (raw.order_by_field) rel.orderByField = raw.order_by_field;
7414
+ if (raw.order_direction) rel.orderDirection = raw.order_direction;
7415
+ return rel;
7416
+ }
7417
+ if (type === "hasManyThrough") {
7418
+ requireField(raw, "model", "hasManyThrough");
7419
+ requireField(raw, "join_model", "hasManyThrough");
7420
+ requireField(raw, "join_model_local_field", "hasManyThrough");
7421
+ requireField(raw, "join_model_related_field", "hasManyThrough");
7422
+ const rel = {
7423
+ type: "hasManyThrough",
7424
+ model: raw.model,
7425
+ joinModel: raw.join_model,
7426
+ joinModelLocalField: raw.join_model_local_field,
7427
+ joinModelRelatedField: raw.join_model_related_field
7428
+ };
7429
+ if (raw.join_model_order_by_field)
7430
+ rel.joinModelOrderByField = raw.join_model_order_by_field;
7431
+ if (raw.join_model_order_direction)
7432
+ rel.joinModelOrderDirection = raw.join_model_order_direction;
7433
+ return rel;
7434
+ }
7435
+ throw new Error(`Unknown relationship type: ${type}`);
7436
+ }
7437
+ function loadSchemaFromTomlString(tomlString) {
7438
+ const parsed = parseToml(tomlString);
7439
+ const models = parsed.models;
7440
+ if (!models || typeof models !== "object") {
7441
+ throw new Error("TOML schema must have a [models] section");
7442
+ }
7443
+ const schemas = [];
7444
+ for (const [modelName, modelDef] of Object.entries(models)) {
7445
+ const fields = {};
7446
+ if (modelDef.fields) {
7447
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
7448
+ fields[fieldName] = parseFieldOptions(fieldDef);
7449
+ }
7450
+ }
7451
+ let relationships;
7452
+ if (modelDef.relationships) {
7453
+ relationships = {};
7454
+ for (const [relName, relDef] of Object.entries(
7455
+ modelDef.relationships
7456
+ )) {
7457
+ relationships[relName] = parseRelationship(relDef);
7458
+ }
7459
+ }
7460
+ let uniqueConstraints;
7461
+ if (modelDef.unique_constraints) {
7462
+ uniqueConstraints = [];
7463
+ for (const raw of modelDef.unique_constraints) {
7464
+ uniqueConstraints.push({
7465
+ name: raw.name,
7466
+ fields: [...raw.fields]
7467
+ });
7468
+ }
7469
+ }
7470
+ schemas.push(
7471
+ defineModelSchema({
7472
+ name: modelName,
7473
+ fields,
7474
+ options: {
7475
+ uniqueConstraints,
7476
+ relationships
7477
+ }
7478
+ })
7479
+ );
7480
+ }
7481
+ return schemas;
7482
+ }
7483
+ async function loadSchemaFromToml(filePath) {
7484
+ const { readFile } = await import("fs/promises");
7485
+ const content = await readFile(filePath, "utf-8");
7486
+ return loadSchemaFromTomlString(content);
7487
+ }
7488
+
7489
+ // src/node.ts
7490
+ init_metaSync();
7491
+
7492
+ // src/utils/yDocDump.ts
7493
+ import * as Y4 from "yjs";
7132
7494
  function toPlain(value) {
7133
- if (value instanceof Y3.Map) {
7495
+ if (value instanceof Y4.Map) {
7134
7496
  const obj = {};
7135
7497
  value.forEach((v, k) => {
7136
7498
  obj[k] = toPlain(v);
7137
7499
  });
7138
7500
  return obj;
7139
7501
  }
7140
- if (value instanceof Y3.Array) {
7502
+ if (value instanceof Y4.Array) {
7141
7503
  return value.toArray().map((item) => toPlain(item));
7142
7504
  }
7143
7505
  return value;
@@ -7146,7 +7508,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7146
7508
  const result = {};
7147
7509
  const includeIndexes = !!options.includeIndexes;
7148
7510
  yDoc.share.forEach((type, name) => {
7149
- if (!(type instanceof Y3.Map)) return;
7511
+ if (!(type instanceof Y4.Map)) return;
7150
7512
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7151
7513
  const records = {};
7152
7514
  type.forEach((recordValue, recordId) => {
@@ -7211,18 +7573,28 @@ export {
7211
7573
  attachAndRegisterModel,
7212
7574
  attachSchemaToClass,
7213
7575
  autoRegisterModel,
7576
+ clearMetaSyncCache,
7214
7577
  constants,
7215
7578
  createModelClass,
7216
7579
  defineModelSchema,
7217
7580
  detectEnvironment,
7581
+ discoverModelNames,
7582
+ discoverSchema,
7218
7583
  dumpYDocToPlain,
7219
7584
  features,
7220
7585
  generateULID,
7221
7586
  getRecommendedNodeEngine,
7587
+ inferFieldType,
7222
7588
  initJsBao,
7223
7589
  isBrowser,
7224
7590
  isNode,
7225
7591
  isNodeModuleAvailable,
7592
+ loadSchemaFromToml,
7593
+ loadSchemaFromTomlString,
7594
+ registerFunctionDefault,
7226
7595
  resetJsBao,
7227
- summarizePlainYDoc
7596
+ schemaToToml,
7597
+ summarizePlainYDoc,
7598
+ syncInferredMeta,
7599
+ syncModelMeta
7228
7600
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-bao",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A library providing data modeling capabilities which support live updates and queries.",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -56,17 +56,17 @@
56
56
  "scripts": {
57
57
  "build": "pnpm build:cli && pnpm codegen && rm -f dist/index.* dist/node.* dist/browser.* dist/cloudflare.* dist/cloudflare-do.* dist/client.* && pnpm build:main",
58
58
  "build:main": "pnpm build:browser && pnpm build:node && pnpm build:universal && pnpm build:cloudflare && pnpm build:cloudflare-do && pnpm build:client",
59
- "build:browser": "tsup src/browser.ts --format esm,cjs --dts --platform browser --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --no-splitting",
59
+ "build:browser": "tsup src/browser.ts --format esm,cjs --dts --platform browser --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --external fs --no-splitting",
60
60
  "build:node": "tsup src/node.ts --format esm,cjs --dts --platform node --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --no-splitting",
61
61
  "build:universal": "tsup src/index.ts --format esm,cjs --dts --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --no-splitting",
62
- "build:cloudflare": "tsup src/cloudflare.ts --format esm,cjs --dts --platform browser --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --no-splitting",
63
- "build:cloudflare-do": "tsup src/cloudflare-do.ts --format esm,cjs --dts --platform neutral --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --no-splitting",
64
- "build:client": "tsup src/client.ts --format esm,cjs --dts --platform browser --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --no-splitting",
62
+ "build:cloudflare": "tsup src/cloudflare.ts --format esm,cjs --dts --platform browser --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --external fs --no-splitting",
63
+ "build:cloudflare-do": "tsup src/cloudflare-do.ts --format esm,cjs --dts --platform neutral --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --external fs --no-splitting",
64
+ "build:client": "tsup src/client.ts --format esm,cjs --dts --platform browser --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --external fs --no-splitting",
65
65
  "build:cli": "tsup src/cli/codegen.ts --format cjs --dts --platform node --target node18 --external commander --external typescript --external fs --external path --external util --no-splitting",
66
66
  "dev": "pnpm build:cli && concurrently \"pnpm codegen:watch\" \"pnpm dev:main\"",
67
67
  "dev:main": "concurrently \"pnpm build:browser:dev --watch\" \"pnpm build:node:dev --watch\" \"pnpm build:universal:dev --watch\"",
68
68
  "dev:simple": "pnpm build:cli && pnpm codegen && pnpm build:main --watch",
69
- "build:browser:dev": "tsup src/browser.ts --format esm,cjs --dts --sourcemap --platform browser --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs",
69
+ "build:browser:dev": "tsup src/browser.ts --format esm,cjs --dts --sourcemap --platform browser --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs --external fs",
70
70
  "build:node:dev": "tsup src/node.ts --format esm,cjs --dts --sourcemap --platform node --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs",
71
71
  "build:universal:dev": "tsup src/index.ts --format esm,cjs --dts --sourcemap --external better-sqlite3 --external sql.js --external async-mutex --external ulid --external yjs",
72
72
  "codegen": "./dist/codegen.cjs --config js-bao.config.cjs || echo 'Codegen skipped (likely in consumer project)'",