pomegranate-db 1.0.0 → 1.1.0

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
@@ -12,6 +12,10 @@
12
12
  Build powerful React and React Native apps that scale from hundreds to tens of thousands of records and remain <em>fast</em> ⚡️
13
13
  </p>
14
14
 
15
+ <p align="center">
16
+ <a href="https://snack.expo.dev/@bobbyquantum/pomegranate-snack">Try the live Expo Snack demo</a>
17
+ </p>
18
+
15
19
  ---
16
20
 
17
21
  ### ⚡️ Instant Launch
@@ -96,6 +96,14 @@ export declare class Database implements ModelDatabaseRef {
96
96
  batch(operations: BatchOperation[]): Promise<void>;
97
97
  /** @internal used by Model */
98
98
  _batch(operations: BatchOperation[]): Promise<void>;
99
+ /** @internal Find a record by table+id for relation resolution */
100
+ _findById(table: string, id: string): Promise<Model | null>;
101
+ /** @internal Observe a record by table+id for relation resolution */
102
+ _observeById(table: string, id: string): Observable<Model | null>;
103
+ /** @internal Fetch related records for has-many relation */
104
+ _fetchRelated(table: string, foreignKey: string, id: string): Promise<Model[]>;
105
+ /** @internal Observe related records for has-many relation */
106
+ _observeRelated(table: string, foreignKey: string, id: string): Observable<Model[]>;
99
107
  /**
100
108
  * Run a sync cycle.
101
109
  * See sync/index.ts for the full implementation.
@@ -221,6 +221,45 @@ class Database {
221
221
  async _batch(operations) {
222
222
  await this._adapter.batch(operations);
223
223
  }
224
+ // ─── Relation Resolution (RelationDatabaseRef) ─────────────────────
225
+ /** @internal Find a record by table+id for relation resolution */
226
+ async _findById(table, id) {
227
+ const collection = this._collections.get(table);
228
+ if (!collection) {
229
+ throw new Error(`No collection registered for table "${table}"`);
230
+ }
231
+ return collection.findById(id);
232
+ }
233
+ /** @internal Observe a record by table+id for relation resolution */
234
+ _observeById(table, id) {
235
+ const collection = this._collections.get(table);
236
+ if (!collection) {
237
+ throw new Error(`No collection registered for table "${table}"`);
238
+ }
239
+ return collection.observeById(id);
240
+ }
241
+ /** @internal Fetch related records for has-many relation */
242
+ async _fetchRelated(table, foreignKey, id) {
243
+ const collection = this._collections.get(table);
244
+ if (!collection) {
245
+ throw new Error(`No collection registered for table "${table}"`);
246
+ }
247
+ const qb = collection.query((q) => {
248
+ q.where(foreignKey, 'eq', id);
249
+ });
250
+ return collection.fetch(qb);
251
+ }
252
+ /** @internal Observe related records for has-many relation */
253
+ _observeRelated(table, foreignKey, id) {
254
+ const collection = this._collections.get(table);
255
+ if (!collection) {
256
+ throw new Error(`No collection registered for table "${table}"`);
257
+ }
258
+ const qb = collection.query((q) => {
259
+ q.where(foreignKey, 'eq', id);
260
+ });
261
+ return collection.observeQuery(qb);
262
+ }
224
263
  // ─── Sync ──────────────────────────────────────────────────────────
225
264
  /**
226
265
  * Run a sync cycle.
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Reactive offline-first database with sync support.
5
5
  */
6
6
  export { m } from './schema';
7
- export type { ColumnType, ColumnDescriptor, TextColumn, NumberColumn, BooleanColumn, DateColumn, BelongsToDescriptor, HasManyDescriptor, RelationDescriptor, FieldDescriptor, SchemaFields, ModelSchema, ResolvedColumn, ResolvedRelation, DatabaseSchema, TableSchema, SyncStatus, RawRecord, InferCreatePatch, InferUpdatePatch, InferRecord, } from './schema';
7
+ export type { ColumnType, ColumnDescriptor, TextColumn, NumberColumn, BooleanColumn, DateColumn, BelongsToDescriptor, HasManyDescriptor, RelationDescriptor, FieldDescriptor, SchemaFields, ModelSchema, ResolvedColumn, ResolvedRelation, DatabaseSchema, TableSchema, SyncStatus, RawRecord, InferCreatePatch, InferUpdatePatch, InferRecord, BelongsToRelation, HasManyRelation, ModelInstance, } from './schema';
8
8
  export { Model } from './model';
9
9
  export type { ModelStatic } from './model';
10
10
  export { Collection } from './collection';
@@ -14,8 +14,9 @@
14
14
  * });
15
15
  * }
16
16
  */
17
- import type { ModelSchema, RawRecord, SyncStatus } from '../schema/types';
17
+ import type { ModelSchema, RawRecord, SyncStatus, BelongsToDescriptor, HasManyDescriptor, BelongsToRelation, HasManyRelation } from '../schema/types';
18
18
  import type { Observable } from '../observable/Subject';
19
+ import type { RelationDatabaseRef } from './Relation';
19
20
  /** Minimal interface for what a Collection provides to a Model */
20
21
  export interface ModelCollectionRef {
21
22
  readonly table: string;
@@ -25,7 +26,7 @@ export interface ModelCollectionRef {
25
26
  _getDatabase(): ModelDatabaseRef;
26
27
  }
27
28
  /** Minimal interface for what a Database provides */
28
- export interface ModelDatabaseRef {
29
+ export interface ModelDatabaseRef extends RelationDatabaseRef {
29
30
  _ensureInWriter(action: string): void;
30
31
  _batch(operations: Array<{
31
32
  type: string;
@@ -56,6 +57,28 @@ export declare class Model<S extends ModelSchema = ModelSchema> {
56
57
  * Get a field value, converting from raw storage form.
57
58
  */
58
59
  getField(fieldName: string): unknown;
60
+ /**
61
+ * Get a typed belongs-to relation handle.
62
+ *
63
+ * The return type is inferred from the schema: if the field is a
64
+ * BelongsToDescriptor<UserSchema>, the return is BelongsToRelation<UserSchema>.
65
+ *
66
+ * Usage in subclass:
67
+ * get author() { return this.belongsTo('author'); }
68
+ * // TS infers: BelongsToRelation<typeof UserSchema>
69
+ */
70
+ belongsTo<K extends keyof S['fields'] & string>(fieldName: K): S['fields'][K] extends BelongsToDescriptor<infer RS> ? BelongsToRelation<RS> : BelongsToRelation;
71
+ /**
72
+ * Get a typed has-many relation handle.
73
+ *
74
+ * The return type is inferred from the schema: if the field is a
75
+ * HasManyDescriptor<CommentSchema>, the return is HasManyRelation<CommentSchema>.
76
+ *
77
+ * Usage in subclass:
78
+ * get comments() { return this.hasMany('comments'); }
79
+ * // TS infers: HasManyRelation<typeof CommentSchema>
80
+ */
81
+ hasMany<K extends keyof S['fields'] & string>(fieldName: K): S['fields'][K] extends HasManyDescriptor<infer RS> ? HasManyRelation<RS> : HasManyRelation;
59
82
  /**
60
83
  * Set field value(s) on the raw record (does NOT persist — internal use).
61
84
  */
@@ -20,6 +20,7 @@ exports.Model = void 0;
20
20
  exports.createRawRecord = createRawRecord;
21
21
  const Subject_1 = require("../observable/Subject");
22
22
  const utils_1 = require("../utils");
23
+ const Relation_1 = require("./Relation");
23
24
  class Model {
24
25
  static schema;
25
26
  /** The record id */
@@ -66,15 +67,54 @@ class Model {
66
67
  const col = schema.columns.find((c) => c.fieldName === fieldName);
67
68
  if (!col) {
68
69
  // Check if it's a relation field name
69
- const rel = schema.relations.find((r) => r.fieldName === fieldName);
70
- if (rel && rel.kind === 'belongs_to') {
71
- return this.#raw[rel.foreignKey];
70
+ const relation = schema.relations.find((r) => r.fieldName === fieldName);
71
+ if (relation && relation.kind === 'belongs_to') {
72
+ return this.#raw[relation.foreignKey];
72
73
  }
73
74
  throw new Error(`Unknown field "${fieldName}" on table "${schema.table}"`);
74
75
  }
75
76
  const rawValue = this.#raw[col.columnName];
76
77
  return deserializeValue(col, rawValue);
77
78
  }
79
+ // ─── Relation Accessors ─────────────────────────────────────────────
80
+ /**
81
+ * Get a typed belongs-to relation handle.
82
+ *
83
+ * The return type is inferred from the schema: if the field is a
84
+ * BelongsToDescriptor<UserSchema>, the return is BelongsToRelation<UserSchema>.
85
+ *
86
+ * Usage in subclass:
87
+ * get author() { return this.belongsTo('author'); }
88
+ * // TS infers: BelongsToRelation<typeof UserSchema>
89
+ */
90
+ belongsTo(fieldName) {
91
+ const schema = this.constructor.schema;
92
+ const relation = schema.relations.find((r) => r.fieldName === fieldName && r.kind === 'belongs_to');
93
+ if (!relation) {
94
+ throw new Error(`No belongs_to relation "${fieldName}" on table "${schema.table}"`);
95
+ }
96
+ const db = this.collection._getDatabase();
97
+ return new Relation_1.BelongsToRelationImpl(() => this.#raw[relation.foreignKey] ?? null, relation._relatedSchemaThunk, db);
98
+ }
99
+ /**
100
+ * Get a typed has-many relation handle.
101
+ *
102
+ * The return type is inferred from the schema: if the field is a
103
+ * HasManyDescriptor<CommentSchema>, the return is HasManyRelation<CommentSchema>.
104
+ *
105
+ * Usage in subclass:
106
+ * get comments() { return this.hasMany('comments'); }
107
+ * // TS infers: HasManyRelation<typeof CommentSchema>
108
+ */
109
+ hasMany(fieldName) {
110
+ const schema = this.constructor.schema;
111
+ const relation = schema.relations.find((r) => r.fieldName === fieldName && r.kind === 'has_many');
112
+ if (!relation) {
113
+ throw new Error(`No has_many relation "${fieldName}" on table "${schema.table}"`);
114
+ }
115
+ const db = this.collection._getDatabase();
116
+ return new Relation_1.HasManyRelationImpl(this.id, relation.foreignKey, relation._relatedSchemaThunk, db);
117
+ }
78
118
  /**
79
119
  * Set field value(s) on the raw record (does NOT persist — internal use).
80
120
  */
@@ -125,10 +165,10 @@ class Model {
125
165
  }
126
166
  else {
127
167
  // Check belongs_to
128
- const rel = schema.relations.find((r) => r.fieldName === fieldName && r.kind === 'belongs_to');
129
- if (rel) {
130
- rawUpdates[rel.foreignKey] = value;
131
- changedColumns.push(rel.foreignKey);
168
+ const relation = schema.relations.find((r) => r.fieldName === fieldName && r.kind === 'belongs_to');
169
+ if (relation) {
170
+ rawUpdates[relation.foreignKey] = value;
171
+ changedColumns.push(relation.foreignKey);
132
172
  }
133
173
  else {
134
174
  throw new Error(`Unknown field "${fieldName}" on table "${schema.table}"`);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Relation wrappers — lazy handles for fetching/observing related records.
3
+ *
4
+ * These are thin facades over Collection primitives. They are created on-demand
5
+ * by Model.belongsTo() and Model.hasMany() and carry the related schema type
6
+ * so TypeScript can infer the related model.
7
+ */
8
+ import type { ModelSchema, BelongsToRelation, HasManyRelation, ModelInstance } from '../schema/types';
9
+ import type { Observable } from '../observable/Subject';
10
+ /**
11
+ * Minimal interface a relation needs to reach the database.
12
+ * Avoids importing Database directly (circular dep).
13
+ */
14
+ export interface RelationDatabaseRef {
15
+ _findById(table: string, id: string): Promise<ModelInstance | null>;
16
+ _observeById(table: string, id: string): Observable<ModelInstance | null>;
17
+ _fetchRelated(table: string, foreignKey: string, id: string): Promise<ModelInstance[]>;
18
+ _observeRelated(table: string, foreignKey: string, id: string): Observable<ModelInstance[]>;
19
+ }
20
+ export declare class BelongsToRelationImpl<S extends ModelSchema = ModelSchema> implements BelongsToRelation<S> {
21
+ private _getFkValue;
22
+ private _relatedSchemaThunk;
23
+ private _db;
24
+ constructor(getFkValue: () => string | null, relatedSchemaThunk: () => S, db: RelationDatabaseRef);
25
+ get id(): string | null;
26
+ fetch(): Promise<ModelInstance<S> | null>;
27
+ observe(): Observable<ModelInstance<S> | null>;
28
+ }
29
+ export declare class HasManyRelationImpl<S extends ModelSchema = ModelSchema> implements HasManyRelation<S> {
30
+ private _ownerId;
31
+ private _foreignKey;
32
+ private _relatedSchemaThunk;
33
+ private _db;
34
+ constructor(ownerId: string, foreignKey: string, relatedSchemaThunk: () => S, db: RelationDatabaseRef);
35
+ fetch(): Promise<ModelInstance<S>[]>;
36
+ observe(): Observable<ModelInstance<S>[]>;
37
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * Relation wrappers — lazy handles for fetching/observing related records.
4
+ *
5
+ * These are thin facades over Collection primitives. They are created on-demand
6
+ * by Model.belongsTo() and Model.hasMany() and carry the related schema type
7
+ * so TypeScript can infer the related model.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.HasManyRelationImpl = exports.BelongsToRelationImpl = void 0;
11
+ const noop = () => { };
12
+ // ─── BelongsToRelationImpl ──────────────────────────────────────────────────
13
+ class BelongsToRelationImpl {
14
+ _getFkValue;
15
+ _relatedSchemaThunk;
16
+ _db;
17
+ constructor(getFkValue, relatedSchemaThunk, db) {
18
+ this._getFkValue = getFkValue;
19
+ this._relatedSchemaThunk = relatedSchemaThunk;
20
+ this._db = db;
21
+ }
22
+ get id() {
23
+ return this._getFkValue();
24
+ }
25
+ async fetch() {
26
+ const fk = this._getFkValue();
27
+ if (!fk)
28
+ return null;
29
+ const table = this._relatedSchemaThunk().table;
30
+ return this._db._findById(table, fk);
31
+ }
32
+ observe() {
33
+ const fk = this._getFkValue();
34
+ if (!fk) {
35
+ return {
36
+ subscribe: (listener) => {
37
+ listener(null);
38
+ return noop;
39
+ },
40
+ };
41
+ }
42
+ const table = this._relatedSchemaThunk().table;
43
+ return this._db._observeById(table, fk);
44
+ }
45
+ }
46
+ exports.BelongsToRelationImpl = BelongsToRelationImpl;
47
+ // ─── HasManyRelationImpl ────────────────────────────────────────────────────
48
+ class HasManyRelationImpl {
49
+ _ownerId;
50
+ _foreignKey;
51
+ _relatedSchemaThunk;
52
+ _db;
53
+ constructor(ownerId, foreignKey, relatedSchemaThunk, db) {
54
+ this._ownerId = ownerId;
55
+ this._foreignKey = foreignKey;
56
+ this._relatedSchemaThunk = relatedSchemaThunk;
57
+ this._db = db;
58
+ }
59
+ async fetch() {
60
+ const table = this._relatedSchemaThunk().table;
61
+ return this._db._fetchRelated(table, this._foreignKey, this._ownerId);
62
+ }
63
+ observe() {
64
+ const table = this._relatedSchemaThunk().table;
65
+ return this._db._observeRelated(table, this._foreignKey, this._ownerId);
66
+ }
67
+ }
68
+ exports.HasManyRelationImpl = HasManyRelationImpl;
69
+ //# sourceMappingURL=Relation.js.map
@@ -1,2 +1,4 @@
1
1
  export { Model, createRawRecord } from './Model';
2
2
  export type { ModelStatic, ModelCollectionRef, ModelDatabaseRef } from './Model';
3
+ export { BelongsToRelationImpl, HasManyRelationImpl } from './Relation';
4
+ export type { RelationDatabaseRef } from './Relation';
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createRawRecord = exports.Model = void 0;
3
+ exports.HasManyRelationImpl = exports.BelongsToRelationImpl = exports.createRawRecord = exports.Model = void 0;
4
4
  var Model_1 = require("./Model");
5
5
  Object.defineProperty(exports, "Model", { enumerable: true, get: function () { return Model_1.Model; } });
6
6
  Object.defineProperty(exports, "createRawRecord", { enumerable: true, get: function () { return Model_1.createRawRecord; } });
7
+ var Relation_1 = require("./Relation");
8
+ Object.defineProperty(exports, "BelongsToRelationImpl", { enumerable: true, get: function () { return Relation_1.BelongsToRelationImpl; } });
9
+ Object.defineProperty(exports, "HasManyRelationImpl", { enumerable: true, get: function () { return Relation_1.HasManyRelationImpl; } });
7
10
  //# sourceMappingURL=index.js.map
@@ -10,8 +10,8 @@
10
10
  * body: m.text(),
11
11
  * isPinned: m.boolean().default(false),
12
12
  * createdAt: m.date('created_at').readonly(),
13
- * author: m.belongsTo('users', { key: 'author_id' }),
14
- * comments: m.hasMany('comments', { foreignKey: 'post_id' }),
13
+ * author: m.belongsTo(() => UserSchema, { key: 'author_id' }),
14
+ * comments: m.hasMany(() => CommentSchema, { foreignKey: 'post_id' }),
15
15
  * });
16
16
  */
17
17
  import type { ColumnDescriptor, TextColumn, NumberColumn, BooleanColumn, DateColumn, BelongsToDescriptor, HasManyDescriptor, ModelSchema, FieldDescriptor } from './types';
@@ -50,13 +50,13 @@ export declare const m: {
50
50
  /** Date column (stored as epoch ms in the database) */
51
51
  date(columnName?: string): ColumnBuilder<DateColumn>;
52
52
  /** Belongs-to relation (many-to-one). Adds a foreign key column. */
53
- belongsTo(relatedTable: string, opts: {
53
+ belongsTo<S extends ModelSchema>(relatedSchema: () => S, opts: {
54
54
  key: string;
55
- }): BelongsToDescriptor;
55
+ }): BelongsToDescriptor<S>;
56
56
  /** Has-many relation (one-to-many). Query-only, no stored column. */
57
- hasMany(relatedTable: string, opts: {
57
+ hasMany<S extends ModelSchema>(relatedSchema: () => S, opts: {
58
58
  foreignKey: string;
59
- }): HasManyDescriptor;
59
+ }): HasManyDescriptor<S>;
60
60
  /**
61
61
  * Define a model schema for the given table.
62
62
  *
@@ -11,8 +11,8 @@
11
11
  * body: m.text(),
12
12
  * isPinned: m.boolean().default(false),
13
13
  * createdAt: m.date('created_at').readonly(),
14
- * author: m.belongsTo('users', { key: 'author_id' }),
15
- * comments: m.hasMany('comments', { foreignKey: 'post_id' }),
14
+ * author: m.belongsTo(() => UserSchema, { key: 'author_id' }),
15
+ * comments: m.hasMany(() => CommentSchema, { foreignKey: 'post_id' }),
16
16
  * });
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -71,8 +71,8 @@ function resolveField(fieldName, raw) {
71
71
  const rel = {
72
72
  fieldName,
73
73
  kind: desc.kind,
74
- relatedTable: desc.relatedTable,
75
74
  foreignKey: desc.foreignKey,
75
+ _relatedSchemaThunk: desc._relatedSchemaThunk,
76
76
  };
77
77
  // belongs_to also implies a column for the foreign key
78
78
  if (desc.kind === 'belongs_to') {
@@ -122,19 +122,19 @@ exports.m = {
122
122
  return makeColumn('date', columnName ?? null);
123
123
  },
124
124
  /** Belongs-to relation (many-to-one). Adds a foreign key column. */
125
- belongsTo(relatedTable, opts) {
125
+ belongsTo(relatedSchema, opts) {
126
126
  return Object.freeze({
127
127
  kind: 'belongs_to',
128
- relatedTable,
129
128
  foreignKey: opts.key,
129
+ _relatedSchemaThunk: relatedSchema,
130
130
  });
131
131
  },
132
132
  /** Has-many relation (one-to-many). Query-only, no stored column. */
133
- hasMany(relatedTable, opts) {
133
+ hasMany(relatedSchema, opts) {
134
134
  return Object.freeze({
135
135
  kind: 'has_many',
136
- relatedTable,
137
136
  foreignKey: opts.foreignKey,
137
+ _relatedSchemaThunk: relatedSchema,
138
138
  });
139
139
  },
140
140
  /**
@@ -1,2 +1,2 @@
1
1
  export { m, ColumnBuilder } from './builder';
2
- export type { ColumnType, ColumnDescriptor, TextColumn, NumberColumn, BooleanColumn, DateColumn, BelongsToDescriptor, HasManyDescriptor, RelationDescriptor, FieldDescriptor, SchemaFields, ModelSchema, ResolvedColumn, ResolvedRelation, DatabaseSchema, TableSchema, TableColumnSchema, SyncStatus, SyncColumns, RawRecord, InferColumnType, InferField, InferCreatePatch, InferUpdatePatch, InferRecord, } from './types';
2
+ export type { ColumnType, ColumnDescriptor, TextColumn, NumberColumn, BooleanColumn, DateColumn, BelongsToDescriptor, HasManyDescriptor, RelationDescriptor, FieldDescriptor, SchemaFields, ModelSchema, ResolvedColumn, ResolvedRelation, DatabaseSchema, TableSchema, TableColumnSchema, SyncStatus, SyncColumns, RawRecord, InferColumnType, InferField, InferCreatePatch, InferUpdatePatch, InferRecord, BelongsToRelation, HasManyRelation, ModelInstance, } from './types';
@@ -26,15 +26,26 @@ export interface DateColumn extends ColumnDescriptor {
26
26
  readonly type: 'date';
27
27
  }
28
28
  export type RelationType = 'belongs_to' | 'has_many';
29
- export interface BelongsToDescriptor {
29
+ /**
30
+ * Belongs-to (many-to-one) relation descriptor.
31
+ * Generic over the related ModelSchema so TypeScript can infer the related type.
32
+ * The thunk `_relatedSchemaThunk` is resolved lazily to support forward references.
33
+ */
34
+ export interface BelongsToDescriptor<S extends ModelSchema = ModelSchema> {
30
35
  readonly kind: 'belongs_to';
31
- readonly relatedTable: string;
32
36
  readonly foreignKey: string;
37
+ /** @internal Lazy reference to the related schema — supports forward references */
38
+ readonly _relatedSchemaThunk: () => S;
33
39
  }
34
- export interface HasManyDescriptor {
40
+ /**
41
+ * Has-many (one-to-many) relation descriptor.
42
+ * Generic over the related ModelSchema so TypeScript can infer the related type.
43
+ */
44
+ export interface HasManyDescriptor<S extends ModelSchema = ModelSchema> {
35
45
  readonly kind: 'has_many';
36
- readonly relatedTable: string;
37
46
  readonly foreignKey: string;
47
+ /** @internal Lazy reference to the related schema — supports forward references */
48
+ readonly _relatedSchemaThunk: () => S;
38
49
  }
39
50
  export type RelationDescriptor = BelongsToDescriptor | HasManyDescriptor;
40
51
  export type FieldDescriptor = ColumnDescriptor | RelationDescriptor;
@@ -59,8 +70,9 @@ export interface ResolvedColumn {
59
70
  export interface ResolvedRelation {
60
71
  readonly fieldName: string;
61
72
  readonly kind: RelationType;
62
- readonly relatedTable: string;
63
73
  readonly foreignKey: string;
74
+ /** @internal Lazy reference — call to get the related schema's table name */
75
+ readonly _relatedSchemaThunk: () => ModelSchema;
64
76
  }
65
77
  export interface DatabaseSchema {
66
78
  readonly version: number;
@@ -76,19 +88,46 @@ export interface TableColumnSchema {
76
88
  readonly isOptional: boolean;
77
89
  readonly isIndexed: boolean;
78
90
  }
91
+ import type { Observable } from '../observable/Subject';
92
+ /** Lazy belongs-to relation handle (many-to-one). */
93
+ export interface BelongsToRelation<S extends ModelSchema = ModelSchema> {
94
+ /** The foreign key value (the related record's ID) */
95
+ readonly id: string | null;
96
+ /** Fetch the related record */
97
+ fetch(): Promise<ModelInstance<S> | null>;
98
+ /** Observe the related record reactively */
99
+ observe(): Observable<ModelInstance<S> | null>;
100
+ }
101
+ /** Lazy has-many relation handle (one-to-many). */
102
+ export interface HasManyRelation<S extends ModelSchema = ModelSchema> {
103
+ /** Fetch all related records */
104
+ fetch(): Promise<ModelInstance<S>[]>;
105
+ /** Observe the related records reactively */
106
+ observe(): Observable<ModelInstance<S>[]>;
107
+ }
108
+ /**
109
+ * A model instance typed by its schema.
110
+ * Forward-declared as a minimal interface to avoid circular imports.
111
+ * Full Model class satisfies this at runtime.
112
+ */
113
+ export interface ModelInstance<S extends ModelSchema = ModelSchema> {
114
+ readonly id: string;
115
+ getField(fieldName: string): unknown;
116
+ observe(): Observable<ModelInstance<S>>;
117
+ }
79
118
  /** Infer the runtime TypeScript type from a ColumnDescriptor */
80
119
  export type InferColumnType<C extends ColumnDescriptor> = C['type'] extends 'text' ? string : C['type'] extends 'number' ? number : C['type'] extends 'boolean' ? boolean : C['type'] extends 'date' ? Date : never;
81
120
  /** For optional columns, make the type T | null */
82
121
  type MaybeOptional<C extends ColumnDescriptor, T> = C['isOptional'] extends true ? T | null : T;
83
- /** Infer column type respecting optionality */
84
- export type InferField<C extends FieldDescriptor> = C extends ColumnDescriptor ? MaybeOptional<C, InferColumnType<C>> : C extends BelongsToDescriptor ? string : C extends HasManyDescriptor ? never : never;
122
+ /** Infer field type columns resolve to values, relations resolve to relation wrappers */
123
+ export type InferField<C extends FieldDescriptor> = C extends ColumnDescriptor ? MaybeOptional<C, InferColumnType<C>> : C extends BelongsToDescriptor<infer S> ? BelongsToRelation<S> : C extends HasManyDescriptor<infer S> ? HasManyRelation<S> : never;
85
124
  /** The record shape inferred from schema fields (writable columns only) */
86
125
  export type InferCreatePatch<F extends SchemaFields> = {
87
126
  [K in keyof F as F[K] extends ColumnDescriptor ? F[K]['isReadonly'] extends true ? never : K : F[K] extends BelongsToDescriptor ? K : never]: F[K] extends ColumnDescriptor ? MaybeOptional<F[K], InferColumnType<F[K]>> : F[K] extends BelongsToDescriptor ? string : never;
88
127
  };
89
128
  /** The record shape for updates — all writable fields optional */
90
129
  export type InferUpdatePatch<F extends SchemaFields> = Partial<InferCreatePatch<F>>;
91
- /** Full record shape (all columns + belongs_to keys) */
130
+ /** Full record shape (all columns + relation wrappers) */
92
131
  export type InferRecord<F extends SchemaFields> = {
93
132
  readonly id: string;
94
133
  } & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pomegranate-db",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Reactive offline-first database with sync support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -62,6 +62,7 @@
62
62
  "build": "tsc && tsc -p expo-plugin/tsconfig.json",
63
63
  "build:lib": "tsc",
64
64
  "build:expo-plugin": "tsc -p expo-plugin/tsconfig.json",
65
+ "pack:demo": "./scripts/pack-demo-tarball",
65
66
  "prepack": "npm run clean && npm run build",
66
67
  "test": "jest",
67
68
  "test:web": "jest --testPathPattern='web\\.test' --no-cache",