js-bao 0.5.0 → 0.5.2

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/index.d.cts CHANGED
@@ -31,8 +31,32 @@ interface FieldOptions {
31
31
  unique?: boolean;
32
32
  default?: any | (() => any);
33
33
  autoAssign?: boolean;
34
+ /**
35
+ * Auto-timestamp this field with `Date.now()` (milliseconds) on save.
36
+ *
37
+ * - `'create'` — stamp only on the first save (when `isNew === true`).
38
+ * - `'update'` — stamp on every save (create AND update).
39
+ * - `'both'` — stamp on every save (create AND update).
40
+ *
41
+ * If the caller passes an explicit value for the field in `data`, the
42
+ * explicit value wins and the stamp is skipped. The stamp is applied
43
+ * in `beforeSave` BEFORE any user-defined hooks, so user hooks may
44
+ * still overwrite the value if they wish (last writer wins).
45
+ */
46
+ autoStamp?: "create" | "update" | "both";
34
47
  maxLength?: number;
35
48
  maxCount?: number;
49
+ /**
50
+ * Allowed-value set for a `string` field. When present, the codegen
51
+ * generators (database-type codegen + doc-model v2 codegen) emit a
52
+ * TypeScript string-literal union (`"a" | "b" | "c"`) instead of a bare
53
+ * `string` for this field.
54
+ *
55
+ * Advisory / codegen-only: this is a TS-emission hint. The runtime and the
56
+ * server do NOT enforce enum membership on write (see #843). Only valid on
57
+ * `string` fields, and must be a non-empty array of strings.
58
+ */
59
+ enum?: string[];
36
60
  }
37
61
  interface UniqueConstraintConfig {
38
62
  name: string;
@@ -529,6 +553,17 @@ declare class BaseModel implements StringSetChangeTracker {
529
553
  private ensureLocalChanges;
530
554
  private hasLocalChange;
531
555
  private getFromYjs;
556
+ /**
557
+ * Defensive normalization for the read/sync boundary (issue #625).
558
+ *
559
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
560
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
561
+ * through untouched. For any other field, a composite Yjs primitive
562
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
563
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
564
+ * warn, rather than throwing. Scalars pass through unchanged.
565
+ */
566
+ private normalizeNonStringSetValue;
532
567
  private getValue;
533
568
  private setValue;
534
569
  get isDirty(): boolean;
@@ -663,6 +698,27 @@ declare class BaseModel implements StringSetChangeTracker {
663
698
  */
664
699
  static count(this: new (...args: any[]) => any, filter?: DocumentFilter, options?: Pick<QueryOptions, "documents">): Promise<number>;
665
700
  static findAll<T extends BaseModel>(this: new (...args: any[]) => T): Promise<T[]>;
701
+ /**
702
+ * Resolve a record by its unique-constraint field values via an engine query.
703
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
704
+ * entry for a key.
705
+ *
706
+ * The index map is a cache over the SQLite projection, not the source of
707
+ * truth: every record is loaded into the projection on `initializeForDocument`
708
+ * regardless of index presence, but a record written by a client predating
709
+ * index maintenance (legacy data) is missing from the index. Such a record
710
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
711
+ * check. A query over the constraint fields finds it reliably.
712
+ *
713
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
714
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
715
+ * whether to heal the missing index entry (write paths) and do their own
716
+ * logging — this method never mutates the index, so read paths stay
717
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
718
+ * constraint field means the record does not participate in the constraint,
719
+ * so no query runs.
720
+ */
721
+ private static _resolveByUniqueFallback;
666
722
  static findByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, value: any | any[]): Promise<T | null>;
667
723
  static upsertByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, uniqueLookupValue: any | any[], dataToUpsert: Partial<Omit<InstanceType<new (...args: any[]) => T>, keyof BaseModel | "toJSON">> & {
668
724
  id?: string;
@@ -835,6 +891,11 @@ declare function attachAndRegisterModel(modelClass: typeof BaseModel, schema: De
835
891
 
836
892
  /**
837
893
  * Infer a _meta_ type string from a JS runtime value.
894
+ *
895
+ * Only an all-`true` Y.Map (the stringset wire shape) is tagged as
896
+ * `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
897
+ * Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
898
+ * than being mis-tagged.
838
899
  */
839
900
  declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
840
901
  /**
@@ -919,6 +980,7 @@ interface DiscoveredField {
919
980
  required?: boolean;
920
981
  default?: string | number | boolean;
921
982
  autoAssign?: boolean;
983
+ autoStamp?: "create" | "update" | "both";
922
984
  maxLength?: number;
923
985
  maxCount?: number;
924
986
  }
@@ -961,6 +1023,41 @@ declare function discoverModelNames(yDoc: Y.Doc): string[];
961
1023
  */
962
1024
  declare function schemaToToml(schema: DiscoveredSchema): string;
963
1025
 
1026
+ /**
1027
+ * Unique-constraint index helpers (shared CRDT contract).
1028
+ *
1029
+ * js-bao enforces unique constraints inside the Yjs document, not via a SQL
1030
+ * `UNIQUE`. Each constraint has a `_uniqueIdx_<model>_<constraint>` map
1031
+ * (uniqueKey -> surviving recordId) and a document observer that
1032
+ * deterministically discards duplicate losers. Both the client write path and
1033
+ * the server-direct write path (`mutateRecord` in js-bao-wss) must derive the
1034
+ * key and the index map name the SAME way, or a server-written record would
1035
+ * not be seen by the client-side resolver. These helpers are the single source
1036
+ * of truth for that derivation so neither side keeps a parallel copy.
1037
+ */
1038
+
1039
+ /**
1040
+ * Build the unique-constraint key for a record over the given fields.
1041
+ *
1042
+ * Mirrors the document-observer conflict resolver: a `null`/`undefined` value
1043
+ * in any field means the record does not participate in the constraint (returns
1044
+ * `null`); otherwise the per-field `String()` values joined by `"|"`. This is
1045
+ * the key the `_uniqueIdx_*` map is read/written with during conflict
1046
+ * resolution, so it is the key the resolver treats as authoritative.
1047
+ */
1048
+ declare function buildUniqueKey(recordData: Y.Map<any> | Record<string, any>, fields: string[]): string | null;
1049
+ /**
1050
+ * Name of the Yjs map that indexes a unique constraint within a document:
1051
+ * `_uniqueIdx_<model>_<constraint>` mapping uniqueKey -> surviving recordId.
1052
+ */
1053
+ declare function uniqueIndexMapName(modelName: string, constraintName: string): string;
1054
+ /**
1055
+ * The constraint name js-bao assigns to a single-field `unique: true` field
1056
+ * (see `resolveUniqueConstraints` in models/schema.ts). Compound constraints
1057
+ * keep their author-given name instead.
1058
+ */
1059
+ declare function fieldUniqueConstraintName(modelName: string, fieldName: string): string;
1060
+
964
1061
  interface DumpOptions {
965
1062
  includeIndexes?: boolean;
966
1063
  }
@@ -997,4 +1094,4 @@ declare function dumpYDocToPlain(yDoc: Y.Doc, options?: DumpOptions): PlainYDoc;
997
1094
  */
998
1095
  declare function summarizePlainYDoc(dump: PlainYDoc): DumpSummary;
999
1096
 
1000
- export { BaseModel, type ConnectedDocument, type DatabaseConfig, DatabaseEngine, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, DocumentClosedError, type DocumentConnectionCallback, type DocumentConnectionEvent, type DocumentFilter, type DocumentManager, type DocumentPermissionHint, DocumentResolutionError, type DumpOptions, type DumpSummary, type DumpSummaryEntry, type FieldOptions, type IncludeSpec, type InferAttrs, type InitJsBaoOptions, type InitJsBaoResult, LogLevel, Logger, type ModelConstructor, type ModelOptions, ModelRegistry, type PaginatedResult, type PaginationOptions, type PlainYDoc, type ProjectionSpec, type QueryOptions, type QueryResult, RecordNotFoundError, type SaveOptions, StringSet, UniqueConstraintViolationError, attachAndRegisterModel, attachSchemaToClass, autoRegisterModel, clearMetaSyncCache, createModelClass, defineModelSchema, discoverModelNames, discoverSchema, dumpYDocToPlain, generateULID, inferFieldType, initJsBao, loadSchemaFromToml, loadSchemaFromTomlString, registerFunctionDefault, resetJsBao, schemaToToml, summarizePlainYDoc, syncInferredMeta, syncModelMeta };
1097
+ export { BaseModel, type ConnectedDocument, type DatabaseConfig, DatabaseEngine, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, DocumentClosedError, type DocumentConnectionCallback, type DocumentConnectionEvent, type DocumentFilter, type DocumentManager, type DocumentPermissionHint, DocumentResolutionError, type DumpOptions, type DumpSummary, type DumpSummaryEntry, type FieldOptions, type IncludeSpec, type InferAttrs, type InitJsBaoOptions, type InitJsBaoResult, LogLevel, Logger, type ModelConstructor, type ModelOptions, ModelRegistry, type PaginatedResult, type PaginationOptions, type PlainYDoc, type ProjectionSpec, type QueryOptions, type QueryResult, RecordNotFoundError, type SaveOptions, StringSet, UniqueConstraintViolationError, attachAndRegisterModel, attachSchemaToClass, autoRegisterModel, buildUniqueKey, clearMetaSyncCache, createModelClass, defineModelSchema, discoverModelNames, discoverSchema, dumpYDocToPlain, fieldUniqueConstraintName, generateULID, inferFieldType, initJsBao, loadSchemaFromToml, loadSchemaFromTomlString, registerFunctionDefault, resetJsBao, schemaToToml, summarizePlainYDoc, syncInferredMeta, syncModelMeta, uniqueIndexMapName };
package/dist/index.d.ts CHANGED
@@ -31,8 +31,32 @@ interface FieldOptions {
31
31
  unique?: boolean;
32
32
  default?: any | (() => any);
33
33
  autoAssign?: boolean;
34
+ /**
35
+ * Auto-timestamp this field with `Date.now()` (milliseconds) on save.
36
+ *
37
+ * - `'create'` — stamp only on the first save (when `isNew === true`).
38
+ * - `'update'` — stamp on every save (create AND update).
39
+ * - `'both'` — stamp on every save (create AND update).
40
+ *
41
+ * If the caller passes an explicit value for the field in `data`, the
42
+ * explicit value wins and the stamp is skipped. The stamp is applied
43
+ * in `beforeSave` BEFORE any user-defined hooks, so user hooks may
44
+ * still overwrite the value if they wish (last writer wins).
45
+ */
46
+ autoStamp?: "create" | "update" | "both";
34
47
  maxLength?: number;
35
48
  maxCount?: number;
49
+ /**
50
+ * Allowed-value set for a `string` field. When present, the codegen
51
+ * generators (database-type codegen + doc-model v2 codegen) emit a
52
+ * TypeScript string-literal union (`"a" | "b" | "c"`) instead of a bare
53
+ * `string` for this field.
54
+ *
55
+ * Advisory / codegen-only: this is a TS-emission hint. The runtime and the
56
+ * server do NOT enforce enum membership on write (see #843). Only valid on
57
+ * `string` fields, and must be a non-empty array of strings.
58
+ */
59
+ enum?: string[];
36
60
  }
37
61
  interface UniqueConstraintConfig {
38
62
  name: string;
@@ -529,6 +553,17 @@ declare class BaseModel implements StringSetChangeTracker {
529
553
  private ensureLocalChanges;
530
554
  private hasLocalChange;
531
555
  private getFromYjs;
556
+ /**
557
+ * Defensive normalization for the read/sync boundary (issue #625).
558
+ *
559
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
560
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
561
+ * through untouched. For any other field, a composite Yjs primitive
562
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
563
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
564
+ * warn, rather than throwing. Scalars pass through unchanged.
565
+ */
566
+ private normalizeNonStringSetValue;
532
567
  private getValue;
533
568
  private setValue;
534
569
  get isDirty(): boolean;
@@ -663,6 +698,27 @@ declare class BaseModel implements StringSetChangeTracker {
663
698
  */
664
699
  static count(this: new (...args: any[]) => any, filter?: DocumentFilter, options?: Pick<QueryOptions, "documents">): Promise<number>;
665
700
  static findAll<T extends BaseModel>(this: new (...args: any[]) => T): Promise<T[]>;
701
+ /**
702
+ * Resolve a record by its unique-constraint field values via an engine query.
703
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
704
+ * entry for a key.
705
+ *
706
+ * The index map is a cache over the SQLite projection, not the source of
707
+ * truth: every record is loaded into the projection on `initializeForDocument`
708
+ * regardless of index presence, but a record written by a client predating
709
+ * index maintenance (legacy data) is missing from the index. Such a record
710
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
711
+ * check. A query over the constraint fields finds it reliably.
712
+ *
713
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
714
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
715
+ * whether to heal the missing index entry (write paths) and do their own
716
+ * logging — this method never mutates the index, so read paths stay
717
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
718
+ * constraint field means the record does not participate in the constraint,
719
+ * so no query runs.
720
+ */
721
+ private static _resolveByUniqueFallback;
666
722
  static findByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, value: any | any[]): Promise<T | null>;
667
723
  static upsertByUnique<T extends BaseModel>(this: typeof BaseModel & (new (...args: any[]) => T), constraintName: string, uniqueLookupValue: any | any[], dataToUpsert: Partial<Omit<InstanceType<new (...args: any[]) => T>, keyof BaseModel | "toJSON">> & {
668
724
  id?: string;
@@ -835,6 +891,11 @@ declare function attachAndRegisterModel(modelClass: typeof BaseModel, schema: De
835
891
 
836
892
  /**
837
893
  * Infer a _meta_ type string from a JS runtime value.
894
+ *
895
+ * Only an all-`true` Y.Map (the stringset wire shape) is tagged as
896
+ * `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
897
+ * Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
898
+ * than being mis-tagged.
838
899
  */
839
900
  declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
840
901
  /**
@@ -919,6 +980,7 @@ interface DiscoveredField {
919
980
  required?: boolean;
920
981
  default?: string | number | boolean;
921
982
  autoAssign?: boolean;
983
+ autoStamp?: "create" | "update" | "both";
922
984
  maxLength?: number;
923
985
  maxCount?: number;
924
986
  }
@@ -961,6 +1023,41 @@ declare function discoverModelNames(yDoc: Y.Doc): string[];
961
1023
  */
962
1024
  declare function schemaToToml(schema: DiscoveredSchema): string;
963
1025
 
1026
+ /**
1027
+ * Unique-constraint index helpers (shared CRDT contract).
1028
+ *
1029
+ * js-bao enforces unique constraints inside the Yjs document, not via a SQL
1030
+ * `UNIQUE`. Each constraint has a `_uniqueIdx_<model>_<constraint>` map
1031
+ * (uniqueKey -> surviving recordId) and a document observer that
1032
+ * deterministically discards duplicate losers. Both the client write path and
1033
+ * the server-direct write path (`mutateRecord` in js-bao-wss) must derive the
1034
+ * key and the index map name the SAME way, or a server-written record would
1035
+ * not be seen by the client-side resolver. These helpers are the single source
1036
+ * of truth for that derivation so neither side keeps a parallel copy.
1037
+ */
1038
+
1039
+ /**
1040
+ * Build the unique-constraint key for a record over the given fields.
1041
+ *
1042
+ * Mirrors the document-observer conflict resolver: a `null`/`undefined` value
1043
+ * in any field means the record does not participate in the constraint (returns
1044
+ * `null`); otherwise the per-field `String()` values joined by `"|"`. This is
1045
+ * the key the `_uniqueIdx_*` map is read/written with during conflict
1046
+ * resolution, so it is the key the resolver treats as authoritative.
1047
+ */
1048
+ declare function buildUniqueKey(recordData: Y.Map<any> | Record<string, any>, fields: string[]): string | null;
1049
+ /**
1050
+ * Name of the Yjs map that indexes a unique constraint within a document:
1051
+ * `_uniqueIdx_<model>_<constraint>` mapping uniqueKey -> surviving recordId.
1052
+ */
1053
+ declare function uniqueIndexMapName(modelName: string, constraintName: string): string;
1054
+ /**
1055
+ * The constraint name js-bao assigns to a single-field `unique: true` field
1056
+ * (see `resolveUniqueConstraints` in models/schema.ts). Compound constraints
1057
+ * keep their author-given name instead.
1058
+ */
1059
+ declare function fieldUniqueConstraintName(modelName: string, fieldName: string): string;
1060
+
964
1061
  interface DumpOptions {
965
1062
  includeIndexes?: boolean;
966
1063
  }
@@ -997,4 +1094,4 @@ declare function dumpYDocToPlain(yDoc: Y.Doc, options?: DumpOptions): PlainYDoc;
997
1094
  */
998
1095
  declare function summarizePlainYDoc(dump: PlainYDoc): DumpSummary;
999
1096
 
1000
- export { BaseModel, type ConnectedDocument, type DatabaseConfig, DatabaseEngine, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, DocumentClosedError, type DocumentConnectionCallback, type DocumentConnectionEvent, type DocumentFilter, type DocumentManager, type DocumentPermissionHint, DocumentResolutionError, type DumpOptions, type DumpSummary, type DumpSummaryEntry, type FieldOptions, type IncludeSpec, type InferAttrs, type InitJsBaoOptions, type InitJsBaoResult, LogLevel, Logger, type ModelConstructor, type ModelOptions, ModelRegistry, type PaginatedResult, type PaginationOptions, type PlainYDoc, type ProjectionSpec, type QueryOptions, type QueryResult, RecordNotFoundError, type SaveOptions, StringSet, UniqueConstraintViolationError, attachAndRegisterModel, attachSchemaToClass, autoRegisterModel, clearMetaSyncCache, createModelClass, defineModelSchema, discoverModelNames, discoverSchema, dumpYDocToPlain, generateULID, inferFieldType, initJsBao, loadSchemaFromToml, loadSchemaFromTomlString, registerFunctionDefault, resetJsBao, schemaToToml, summarizePlainYDoc, syncInferredMeta, syncModelMeta };
1097
+ export { BaseModel, type ConnectedDocument, type DatabaseConfig, DatabaseEngine, type DiscoveredConstraint, type DiscoveredField, type DiscoveredModel, type DiscoveredRelationship, type DiscoveredSchema, DocumentClosedError, type DocumentConnectionCallback, type DocumentConnectionEvent, type DocumentFilter, type DocumentManager, type DocumentPermissionHint, DocumentResolutionError, type DumpOptions, type DumpSummary, type DumpSummaryEntry, type FieldOptions, type IncludeSpec, type InferAttrs, type InitJsBaoOptions, type InitJsBaoResult, LogLevel, Logger, type ModelConstructor, type ModelOptions, ModelRegistry, type PaginatedResult, type PaginationOptions, type PlainYDoc, type ProjectionSpec, type QueryOptions, type QueryResult, RecordNotFoundError, type SaveOptions, StringSet, UniqueConstraintViolationError, attachAndRegisterModel, attachSchemaToClass, autoRegisterModel, buildUniqueKey, clearMetaSyncCache, createModelClass, defineModelSchema, discoverModelNames, discoverSchema, dumpYDocToPlain, fieldUniqueConstraintName, generateULID, inferFieldType, initJsBao, loadSchemaFromToml, loadSchemaFromTomlString, registerFunctionDefault, resetJsBao, schemaToToml, summarizePlainYDoc, syncInferredMeta, syncModelMeta, uniqueIndexMapName };