pomegranate-db 0.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/LICENSE +21 -0
- package/NOTICE.md +38 -0
- package/PomegranateDB.podspec +67 -0
- package/README.md +122 -0
- package/dist/adapters/expo-sqlite/ExpoSQLiteDriver.d.ts +34 -0
- package/dist/adapters/expo-sqlite/ExpoSQLiteDriver.js +155 -0
- package/dist/adapters/expo-sqlite/index.d.ts +2 -0
- package/dist/adapters/expo-sqlite/index.js +6 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +13 -0
- package/dist/adapters/loki/LokiAdapter.d.ts +100 -0
- package/dist/adapters/loki/LokiAdapter.js +144 -0
- package/dist/adapters/loki/index.d.ts +6 -0
- package/dist/adapters/loki/index.js +12 -0
- package/dist/adapters/loki/worker/LokiDispatcher.d.ts +21 -0
- package/dist/adapters/loki/worker/LokiDispatcher.js +63 -0
- package/dist/adapters/loki/worker/LokiExecutor.d.ts +96 -0
- package/dist/adapters/loki/worker/LokiExecutor.js +462 -0
- package/dist/adapters/loki/worker/SynchronousWorker.d.ts +22 -0
- package/dist/adapters/loki/worker/SynchronousWorker.js +76 -0
- package/dist/adapters/loki/worker/loki.worker.d.ts +14 -0
- package/dist/adapters/loki/worker/loki.worker.js +112 -0
- package/dist/adapters/loki/worker/types.d.ts +44 -0
- package/dist/adapters/loki/worker/types.js +11 -0
- package/dist/adapters/native-sqlite/NativeSQLiteDriver.d.ts +55 -0
- package/dist/adapters/native-sqlite/NativeSQLiteDriver.js +145 -0
- package/dist/adapters/native-sqlite/index.d.ts +2 -0
- package/dist/adapters/native-sqlite/index.js +6 -0
- package/dist/adapters/op-sqlite/OpSQLiteDriver.d.ts +49 -0
- package/dist/adapters/op-sqlite/OpSQLiteDriver.js +140 -0
- package/dist/adapters/op-sqlite/index.d.ts +2 -0
- package/dist/adapters/op-sqlite/index.js +6 -0
- package/dist/adapters/sqlite/SQLiteAdapter.d.ts +70 -0
- package/dist/adapters/sqlite/SQLiteAdapter.js +264 -0
- package/dist/adapters/sqlite/index.d.ts +2 -0
- package/dist/adapters/sqlite/index.js +6 -0
- package/dist/adapters/sqlite/sql.d.ts +35 -0
- package/dist/adapters/sqlite/sql.js +258 -0
- package/dist/adapters/types.d.ts +93 -0
- package/dist/adapters/types.js +9 -0
- package/dist/collection/Collection.d.ts +103 -0
- package/dist/collection/Collection.js +245 -0
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +6 -0
- package/dist/database/Database.d.ts +128 -0
- package/dist/database/Database.js +245 -0
- package/dist/database/index.d.ts +2 -0
- package/dist/database/index.js +6 -0
- package/dist/encryption/index.d.ts +62 -0
- package/dist/encryption/index.js +276 -0
- package/dist/encryption/nodeCrypto.d.ts +18 -0
- package/dist/encryption/nodeCrypto.js +25 -0
- package/dist/encryption/nodeCrypto.native.d.ts +13 -0
- package/dist/encryption/nodeCrypto.native.js +26 -0
- package/dist/expo.d.ts +12 -0
- package/dist/expo.js +32 -0
- package/dist/hooks/index.d.ts +115 -0
- package/dist/hooks/index.js +285 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +57 -0
- package/dist/model/Model.d.ts +92 -0
- package/dist/model/Model.js +251 -0
- package/dist/model/index.d.ts +2 -0
- package/dist/model/index.js +7 -0
- package/dist/observable/Subject.d.ts +60 -0
- package/dist/observable/Subject.js +132 -0
- package/dist/observable/index.d.ts +2 -0
- package/dist/observable/index.js +10 -0
- package/dist/query/QueryBuilder.d.ts +51 -0
- package/dist/query/QueryBuilder.js +165 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +7 -0
- package/dist/query/types.d.ts +60 -0
- package/dist/query/types.js +9 -0
- package/dist/schema/builder.d.ts +68 -0
- package/dist/schema/builder.js +168 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/types.d.ts +108 -0
- package/dist/schema/types.js +9 -0
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +6 -0
- package/dist/sync/sync.d.ts +15 -0
- package/dist/sync/sync.js +182 -0
- package/dist/sync/types.d.ts +41 -0
- package/dist/sync/types.js +6 -0
- package/dist/utils/index.d.ts +45 -0
- package/dist/utils/index.js +99 -0
- package/expo-plugin/index.d.ts +68 -0
- package/expo-plugin/index.js +83 -0
- package/native/android-jsi/build.gradle +45 -0
- package/native/android-jsi/src/main/AndroidManifest.xml +2 -0
- package/native/android-jsi/src/main/cpp/CMakeLists.txt +73 -0
- package/native/android-jsi/src/main/cpp/DatabasePlatformAndroid.cpp +107 -0
- package/native/android-jsi/src/main/cpp/DatabasePlatformAndroid.h +16 -0
- package/native/android-jsi/src/main/cpp/JSIInstaller.cpp +27 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/JSIInstaller.kt +43 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/PomegranateJSIModule.kt +39 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/PomegranateJSIPackage.kt +17 -0
- package/native/ios/DatabasePlatformIOS.mm +83 -0
- package/native/ios/PomegranateJSI.h +15 -0
- package/native/ios/PomegranateJSI.mm +59 -0
- package/native/shared/Database.cpp +283 -0
- package/native/shared/Database.h +84 -0
- package/native/shared/Sqlite.cpp +61 -0
- package/native/shared/Sqlite.h +67 -0
- package/native/shared/sqlite3/sqlite3.c +260493 -0
- package/native/shared/sqlite3/sqlite3.h +13583 -0
- package/package.json +127 -0
- package/react-native.config.js +28 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Model base class.
|
|
4
|
+
*
|
|
5
|
+
* Each model instance represents a single database record.
|
|
6
|
+
* Models are schema-first: the schema defines columns, relations,
|
|
7
|
+
* and the TypeScript types are inferred from it.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* class Post extends Model<typeof PostSchema> {
|
|
11
|
+
* static schema = PostSchema;
|
|
12
|
+
*
|
|
13
|
+
* publish = this.writer(async () => {
|
|
14
|
+
* await this.update({ status: 'published' });
|
|
15
|
+
* });
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.Model = void 0;
|
|
20
|
+
exports.createRawRecord = createRawRecord;
|
|
21
|
+
const Subject_1 = require("../observable/Subject");
|
|
22
|
+
const utils_1 = require("../utils");
|
|
23
|
+
class Model {
|
|
24
|
+
static schema;
|
|
25
|
+
/** The record id */
|
|
26
|
+
id;
|
|
27
|
+
/** Reference back to the owning collection */
|
|
28
|
+
collection;
|
|
29
|
+
/** The raw database row — ES private to avoid variance issues in generics */
|
|
30
|
+
#raw;
|
|
31
|
+
/**
|
|
32
|
+
* Observable for record changes.
|
|
33
|
+
* Typed as `BehaviorSubject<unknown>` to avoid TypeScript variance issues:
|
|
34
|
+
* `BehaviorSubject<this>` causes invariant `Set<Listener<T>>` mismatches
|
|
35
|
+
* when a subclass (e.g. Article) is assigned to `ModelStatic<Model>`.
|
|
36
|
+
* The public API (`observe()`) still returns `Observable<this>`.
|
|
37
|
+
*/
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
#changes$;
|
|
40
|
+
constructor(collection, raw) {
|
|
41
|
+
this.collection = collection;
|
|
42
|
+
this.#raw = raw;
|
|
43
|
+
this.id = raw.id;
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
this.#changes$ = new Subject_1.BehaviorSubject(this);
|
|
46
|
+
}
|
|
47
|
+
// ─── Raw Access ────────────────────────────────────────────────────────
|
|
48
|
+
/** Get the current raw record */
|
|
49
|
+
get _rawRecord() {
|
|
50
|
+
return this.#raw;
|
|
51
|
+
}
|
|
52
|
+
/** Sync status of this record */
|
|
53
|
+
get syncStatus() {
|
|
54
|
+
return this.#raw._status;
|
|
55
|
+
}
|
|
56
|
+
/** Changed fields (comma-separated) */
|
|
57
|
+
get changedFields() {
|
|
58
|
+
return this.#raw._changed;
|
|
59
|
+
}
|
|
60
|
+
// ─── Field Accessors ──────────────────────────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* Get a field value, converting from raw storage form.
|
|
63
|
+
*/
|
|
64
|
+
getField(fieldName) {
|
|
65
|
+
const schema = this.constructor.schema;
|
|
66
|
+
const col = schema.columns.find((c) => c.fieldName === fieldName);
|
|
67
|
+
if (!col) {
|
|
68
|
+
// 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];
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`Unknown field "${fieldName}" on table "${schema.table}"`);
|
|
74
|
+
}
|
|
75
|
+
const rawValue = this.#raw[col.columnName];
|
|
76
|
+
return deserializeValue(col, rawValue);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Set field value(s) on the raw record (does NOT persist — internal use).
|
|
80
|
+
*/
|
|
81
|
+
_setRaw(updates) {
|
|
82
|
+
this.#raw = { ...this.#raw, ...updates };
|
|
83
|
+
this.#changes$.next(this);
|
|
84
|
+
}
|
|
85
|
+
// ─── Observable ──────────────────────────────────────────────────────
|
|
86
|
+
/** Observe changes to this record */
|
|
87
|
+
observe() {
|
|
88
|
+
return this.#changes$;
|
|
89
|
+
}
|
|
90
|
+
/** Observe a specific field */
|
|
91
|
+
observeField(fieldName) {
|
|
92
|
+
return {
|
|
93
|
+
subscribe: (listener) => {
|
|
94
|
+
let lastValue = this.getField(fieldName);
|
|
95
|
+
// Emit initial value
|
|
96
|
+
listener(lastValue);
|
|
97
|
+
return this.#changes$.subscribe(() => {
|
|
98
|
+
const newValue = this.getField(fieldName);
|
|
99
|
+
if (newValue !== lastValue) {
|
|
100
|
+
lastValue = newValue;
|
|
101
|
+
listener(newValue);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// ─── Mutations ────────────────────────────────────────────────────────
|
|
108
|
+
/**
|
|
109
|
+
* Update this record with a patch of field values.
|
|
110
|
+
* Must be called inside `db.write()`.
|
|
111
|
+
*/
|
|
112
|
+
async update(patch) {
|
|
113
|
+
this.collection._getDatabase()._ensureInWriter('Model.update()');
|
|
114
|
+
const schema = this.constructor.schema;
|
|
115
|
+
const rawUpdates = {};
|
|
116
|
+
const changedColumns = [];
|
|
117
|
+
for (const [fieldName, value] of Object.entries(patch)) {
|
|
118
|
+
const col = schema.columns.find((c) => c.fieldName === fieldName);
|
|
119
|
+
if (col) {
|
|
120
|
+
if (col.isReadonly) {
|
|
121
|
+
throw new Error(`Cannot update readonly field "${fieldName}"`);
|
|
122
|
+
}
|
|
123
|
+
rawUpdates[col.columnName] = serializeValue(col, value);
|
|
124
|
+
changedColumns.push(col.columnName);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// 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);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
throw new Error(`Unknown field "${fieldName}" on table "${schema.table}"`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Track changed columns for sync
|
|
139
|
+
const existingChanged = this.#raw._changed ? this.#raw._changed.split(',').filter(Boolean) : [];
|
|
140
|
+
const allChanged = [...new Set([...existingChanged, ...changedColumns])];
|
|
141
|
+
if (this.#raw._status === 'synced') {
|
|
142
|
+
rawUpdates._status = 'updated';
|
|
143
|
+
}
|
|
144
|
+
rawUpdates._changed = allChanged.join(',');
|
|
145
|
+
await this.collection._update(this.id, rawUpdates);
|
|
146
|
+
this._setRaw(rawUpdates);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Mark this record as deleted (soft delete for sync).
|
|
150
|
+
* Must be called inside `db.write()`.
|
|
151
|
+
*/
|
|
152
|
+
async markAsDeleted() {
|
|
153
|
+
this.collection._getDatabase()._ensureInWriter('Model.markAsDeleted()');
|
|
154
|
+
await this.collection._delete(this.id);
|
|
155
|
+
this._setRaw({ _status: 'deleted' });
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Permanently destroy this record.
|
|
159
|
+
* Must be called inside `db.write()`.
|
|
160
|
+
*/
|
|
161
|
+
async destroyPermanently() {
|
|
162
|
+
this.collection._getDatabase()._ensureInWriter('Model.destroyPermanently()');
|
|
163
|
+
await this.collection._destroyPermanently(this.id);
|
|
164
|
+
}
|
|
165
|
+
// ─── Writer Helper ──────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Create a bound writer method.
|
|
168
|
+
* The returned function, when called, will run inside the current write transaction.
|
|
169
|
+
*/
|
|
170
|
+
writer(fn) {
|
|
171
|
+
return (...args) => {
|
|
172
|
+
// Writer just delegates — the caller must already be inside db.write()
|
|
173
|
+
return fn.apply(this, args);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// ─── Prepare for sync ───────────────────────────────────────────────
|
|
177
|
+
/**
|
|
178
|
+
* Return raw values suitable for the sync push payload.
|
|
179
|
+
*/
|
|
180
|
+
toPushPayload() {
|
|
181
|
+
const { _status, _changed, ...rest } = this.#raw;
|
|
182
|
+
return rest;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.Model = Model;
|
|
186
|
+
// ─── Serialization Helpers ─────────────────────────────────────────────────
|
|
187
|
+
function serializeValue(col, value) {
|
|
188
|
+
if (value === null || value === undefined) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
switch (col.type) {
|
|
192
|
+
case 'date':
|
|
193
|
+
return (0, utils_1.dateToTimestamp)(value);
|
|
194
|
+
case 'boolean':
|
|
195
|
+
return value ? 1 : 0;
|
|
196
|
+
default:
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function deserializeValue(col, rawValue) {
|
|
201
|
+
if (rawValue === null || rawValue === undefined) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
switch (col.type) {
|
|
205
|
+
case 'date':
|
|
206
|
+
return (0, utils_1.timestampToDate)(rawValue);
|
|
207
|
+
case 'boolean':
|
|
208
|
+
return rawValue === 1 || rawValue === true;
|
|
209
|
+
default:
|
|
210
|
+
return rawValue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ─── Create a raw record from a schema + patch ────────────────────────────
|
|
214
|
+
function createRawRecord(schema, patch, id) {
|
|
215
|
+
const raw = {
|
|
216
|
+
id: id ?? (0, utils_1.generateId)(),
|
|
217
|
+
_status: 'created',
|
|
218
|
+
_changed: '',
|
|
219
|
+
};
|
|
220
|
+
for (const col of schema.columns) {
|
|
221
|
+
const fieldName = col.fieldName;
|
|
222
|
+
if (fieldName in patch) {
|
|
223
|
+
raw[col.columnName] = serializeValue(col, patch[fieldName]);
|
|
224
|
+
}
|
|
225
|
+
else if (col.defaultValue !== undefined) {
|
|
226
|
+
raw[col.columnName] = serializeValue(col, col.defaultValue);
|
|
227
|
+
}
|
|
228
|
+
else if (col.isOptional) {
|
|
229
|
+
raw[col.columnName] = null;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Default zero-values
|
|
233
|
+
switch (col.type) {
|
|
234
|
+
case 'text':
|
|
235
|
+
raw[col.columnName] = '';
|
|
236
|
+
break;
|
|
237
|
+
case 'number':
|
|
238
|
+
raw[col.columnName] = 0;
|
|
239
|
+
break;
|
|
240
|
+
case 'boolean':
|
|
241
|
+
raw[col.columnName] = 0;
|
|
242
|
+
break;
|
|
243
|
+
case 'date':
|
|
244
|
+
raw[col.columnName] = null;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return raw;
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=Model.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRawRecord = exports.Model = void 0;
|
|
4
|
+
var Model_1 = require("./Model");
|
|
5
|
+
Object.defineProperty(exports, "Model", { enumerable: true, get: function () { return Model_1.Model; } });
|
|
6
|
+
Object.defineProperty(exports, "createRawRecord", { enumerable: true, get: function () { return Model_1.createRawRecord; } });
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight observable primitives.
|
|
3
|
+
*
|
|
4
|
+
* We avoid pulling in RxJS by implementing a minimal Subject/Observable
|
|
5
|
+
* that covers the use-cases we need: change notification for records,
|
|
6
|
+
* collections, and live queries.
|
|
7
|
+
*/
|
|
8
|
+
export type Listener<T> = (value: T) => void;
|
|
9
|
+
export type Unsubscribe = () => void;
|
|
10
|
+
export type TeardownFn = () => void;
|
|
11
|
+
/**
|
|
12
|
+
* A minimal Observable that supports subscribe/unsubscribe.
|
|
13
|
+
*/
|
|
14
|
+
export interface Observable<T> {
|
|
15
|
+
subscribe(listener: Listener<T>): Unsubscribe;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A Subject is an Observable that you can push values into.
|
|
19
|
+
*
|
|
20
|
+
* NOTE: We use ES `#private` fields (not TypeScript `private`) so that the
|
|
21
|
+
* internal `Set<Listener<T>>` is invisible to the structural type checker.
|
|
22
|
+
* Without this, `Subject<Post>` is not assignable to `Subject<Model>` because
|
|
23
|
+
* `Set` is invariant — a classic generic-variance pitfall.
|
|
24
|
+
*/
|
|
25
|
+
export declare class Subject<T> implements Observable<T> {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(initialValue?: T);
|
|
28
|
+
get lastValue(): T | undefined;
|
|
29
|
+
get hasValue(): boolean;
|
|
30
|
+
subscribe(listener: Listener<T>): Unsubscribe;
|
|
31
|
+
next(value: T): void;
|
|
32
|
+
get subscriberCount(): number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A BehaviorSubject always has a current value and emits it to new subscribers.
|
|
36
|
+
*/
|
|
37
|
+
export declare class BehaviorSubject<T> extends Subject<T> {
|
|
38
|
+
constructor(initialValue: T);
|
|
39
|
+
get value(): T;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* SharedObservable caches the latest result and replays to new subscribers.
|
|
43
|
+
* Runs a producer function when the first subscriber appears,
|
|
44
|
+
* and tears down when the last subscriber leaves.
|
|
45
|
+
*/
|
|
46
|
+
export declare class SharedObservable<T> implements Observable<T> {
|
|
47
|
+
private producer;
|
|
48
|
+
private subject;
|
|
49
|
+
private teardown;
|
|
50
|
+
constructor(producer: (emit: Listener<T>) => TeardownFn);
|
|
51
|
+
subscribe(listener: Listener<T>): Unsubscribe;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Utility: map an observable to a new observable.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mapObservable<A, B>(source: Observable<A>, fn: (value: A) => B): Observable<B>;
|
|
57
|
+
/**
|
|
58
|
+
* Utility: combine multiple observables into one that emits an array.
|
|
59
|
+
*/
|
|
60
|
+
export declare function combineObservables<T>(sources: Observable<T>[]): Observable<T[]>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight observable primitives.
|
|
4
|
+
*
|
|
5
|
+
* We avoid pulling in RxJS by implementing a minimal Subject/Observable
|
|
6
|
+
* that covers the use-cases we need: change notification for records,
|
|
7
|
+
* collections, and live queries.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.SharedObservable = exports.BehaviorSubject = exports.Subject = void 0;
|
|
11
|
+
exports.mapObservable = mapObservable;
|
|
12
|
+
exports.combineObservables = combineObservables;
|
|
13
|
+
/**
|
|
14
|
+
* A Subject is an Observable that you can push values into.
|
|
15
|
+
*
|
|
16
|
+
* NOTE: We use ES `#private` fields (not TypeScript `private`) so that the
|
|
17
|
+
* internal `Set<Listener<T>>` is invisible to the structural type checker.
|
|
18
|
+
* Without this, `Subject<Post>` is not assignable to `Subject<Model>` because
|
|
19
|
+
* `Set` is invariant — a classic generic-variance pitfall.
|
|
20
|
+
*/
|
|
21
|
+
class Subject {
|
|
22
|
+
#listeners = new Set();
|
|
23
|
+
#lastValue;
|
|
24
|
+
#hasValue = false;
|
|
25
|
+
constructor(initialValue) {
|
|
26
|
+
if (arguments.length > 0) {
|
|
27
|
+
this.#lastValue = initialValue;
|
|
28
|
+
this.#hasValue = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
get lastValue() {
|
|
32
|
+
return this.#lastValue;
|
|
33
|
+
}
|
|
34
|
+
get hasValue() {
|
|
35
|
+
return this.#hasValue;
|
|
36
|
+
}
|
|
37
|
+
subscribe(listener) {
|
|
38
|
+
this.#listeners.add(listener);
|
|
39
|
+
// Immediately emit last value if we have one
|
|
40
|
+
if (this.#hasValue) {
|
|
41
|
+
listener(this.#lastValue);
|
|
42
|
+
}
|
|
43
|
+
return () => {
|
|
44
|
+
this.#listeners.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
next(value) {
|
|
48
|
+
this.#lastValue = value;
|
|
49
|
+
this.#hasValue = true;
|
|
50
|
+
for (const listener of this.#listeners) {
|
|
51
|
+
listener(value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
get subscriberCount() {
|
|
55
|
+
return this.#listeners.size;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.Subject = Subject;
|
|
59
|
+
/**
|
|
60
|
+
* A BehaviorSubject always has a current value and emits it to new subscribers.
|
|
61
|
+
*/
|
|
62
|
+
class BehaviorSubject extends Subject {
|
|
63
|
+
constructor(initialValue) {
|
|
64
|
+
super(initialValue);
|
|
65
|
+
}
|
|
66
|
+
get value() {
|
|
67
|
+
return this.lastValue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.BehaviorSubject = BehaviorSubject;
|
|
71
|
+
/**
|
|
72
|
+
* SharedObservable caches the latest result and replays to new subscribers.
|
|
73
|
+
* Runs a producer function when the first subscriber appears,
|
|
74
|
+
* and tears down when the last subscriber leaves.
|
|
75
|
+
*/
|
|
76
|
+
class SharedObservable {
|
|
77
|
+
producer;
|
|
78
|
+
subject = null;
|
|
79
|
+
teardown = null;
|
|
80
|
+
constructor(producer) {
|
|
81
|
+
this.producer = producer;
|
|
82
|
+
}
|
|
83
|
+
subscribe(listener) {
|
|
84
|
+
if (!this.subject) {
|
|
85
|
+
this.subject = new Subject();
|
|
86
|
+
this.teardown = this.producer((value) => this.subject.next(value));
|
|
87
|
+
}
|
|
88
|
+
const unsub = this.subject.subscribe(listener);
|
|
89
|
+
return () => {
|
|
90
|
+
unsub();
|
|
91
|
+
if (this.subject && this.subject.subscriberCount === 0) {
|
|
92
|
+
this.teardown?.();
|
|
93
|
+
this.teardown = null;
|
|
94
|
+
this.subject = null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.SharedObservable = SharedObservable;
|
|
100
|
+
/**
|
|
101
|
+
* Utility: map an observable to a new observable.
|
|
102
|
+
*/
|
|
103
|
+
function mapObservable(source, fn) {
|
|
104
|
+
return {
|
|
105
|
+
subscribe(listener) {
|
|
106
|
+
return source.subscribe((value) => listener(fn(value)));
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Utility: combine multiple observables into one that emits an array.
|
|
112
|
+
*/
|
|
113
|
+
function combineObservables(sources) {
|
|
114
|
+
return {
|
|
115
|
+
subscribe(listener) {
|
|
116
|
+
const values = Array.from({ length: sources.length });
|
|
117
|
+
const received = new Set();
|
|
118
|
+
const unsubs = [];
|
|
119
|
+
sources.forEach((source, i) => {
|
|
120
|
+
unsubs.push(source.subscribe((value) => {
|
|
121
|
+
values[i] = value;
|
|
122
|
+
received.add(i);
|
|
123
|
+
if (received.size === sources.length) {
|
|
124
|
+
listener([...values]);
|
|
125
|
+
}
|
|
126
|
+
}));
|
|
127
|
+
});
|
|
128
|
+
return () => unsubs.forEach((u) => u());
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=Subject.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.combineObservables = exports.mapObservable = exports.SharedObservable = exports.BehaviorSubject = exports.Subject = void 0;
|
|
4
|
+
var Subject_1 = require("./Subject");
|
|
5
|
+
Object.defineProperty(exports, "Subject", { enumerable: true, get: function () { return Subject_1.Subject; } });
|
|
6
|
+
Object.defineProperty(exports, "BehaviorSubject", { enumerable: true, get: function () { return Subject_1.BehaviorSubject; } });
|
|
7
|
+
Object.defineProperty(exports, "SharedObservable", { enumerable: true, get: function () { return Subject_1.SharedObservable; } });
|
|
8
|
+
Object.defineProperty(exports, "mapObservable", { enumerable: true, get: function () { return Subject_1.mapObservable; } });
|
|
9
|
+
Object.defineProperty(exports, "combineObservables", { enumerable: true, get: function () { return Subject_1.combineObservables; } });
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query builder — fluent API for constructing QueryDescriptors.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const q = new QueryBuilder('posts')
|
|
6
|
+
* .where('status', 'eq', 'published')
|
|
7
|
+
* .where('createdAt', 'gt', someDate)
|
|
8
|
+
* .orderBy('createdAt', 'desc')
|
|
9
|
+
* .limit(20)
|
|
10
|
+
* .build();
|
|
11
|
+
*/
|
|
12
|
+
import type { QueryDescriptor, ComparisonOperator, SortOrder } from './types';
|
|
13
|
+
export declare class QueryBuilder {
|
|
14
|
+
private _table;
|
|
15
|
+
private _conditions;
|
|
16
|
+
private _orderBy;
|
|
17
|
+
private _limit?;
|
|
18
|
+
private _offset?;
|
|
19
|
+
private _joins;
|
|
20
|
+
constructor(table: string);
|
|
21
|
+
/** Add a WHERE condition */
|
|
22
|
+
where(column: string, value: unknown): this;
|
|
23
|
+
where(column: string, operator: ComparisonOperator, value: unknown): this;
|
|
24
|
+
/** WHERE column IS NULL */
|
|
25
|
+
whereNull(column: string): this;
|
|
26
|
+
/** WHERE column IS NOT NULL */
|
|
27
|
+
whereNotNull(column: string): this;
|
|
28
|
+
/** WHERE column IN (...values) */
|
|
29
|
+
whereIn(column: string, values: unknown[]): this;
|
|
30
|
+
/** WHERE column BETWEEN low AND high */
|
|
31
|
+
whereBetween(column: string, low: unknown, high: unknown): this;
|
|
32
|
+
/** WHERE column LIKE pattern */
|
|
33
|
+
whereLike(column: string, pattern: string): this;
|
|
34
|
+
/** Combine conditions with AND */
|
|
35
|
+
and(builder: (qb: QueryBuilder) => void): this;
|
|
36
|
+
/** Combine conditions with OR */
|
|
37
|
+
or(builder: (qb: QueryBuilder) => void): this;
|
|
38
|
+
/** Add ORDER BY */
|
|
39
|
+
orderBy(column: string, order?: SortOrder): this;
|
|
40
|
+
/** Set LIMIT */
|
|
41
|
+
limit(n: number): this;
|
|
42
|
+
/** Set OFFSET */
|
|
43
|
+
offset(n: number): this;
|
|
44
|
+
/** Add a JOIN clause */
|
|
45
|
+
join(table: string, leftColumn: string, rightColumn: string): this;
|
|
46
|
+
/** Build the final query descriptor */
|
|
47
|
+
build(): QueryDescriptor;
|
|
48
|
+
/** Clone this builder for forking */
|
|
49
|
+
clone(): QueryBuilder;
|
|
50
|
+
}
|
|
51
|
+
export declare function query(table: string): QueryBuilder;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Query builder — fluent API for constructing QueryDescriptors.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* const q = new QueryBuilder('posts')
|
|
7
|
+
* .where('status', 'eq', 'published')
|
|
8
|
+
* .where('createdAt', 'gt', someDate)
|
|
9
|
+
* .orderBy('createdAt', 'desc')
|
|
10
|
+
* .limit(20)
|
|
11
|
+
* .build();
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.QueryBuilder = void 0;
|
|
15
|
+
exports.query = query;
|
|
16
|
+
class QueryBuilder {
|
|
17
|
+
_table;
|
|
18
|
+
_conditions = [];
|
|
19
|
+
_orderBy = [];
|
|
20
|
+
_limit;
|
|
21
|
+
_offset;
|
|
22
|
+
_joins = [];
|
|
23
|
+
constructor(table) {
|
|
24
|
+
this._table = table;
|
|
25
|
+
}
|
|
26
|
+
where(column, operatorOrValue, maybeValue) {
|
|
27
|
+
let operator;
|
|
28
|
+
let value;
|
|
29
|
+
if (maybeValue === undefined) {
|
|
30
|
+
operator = 'eq';
|
|
31
|
+
value = operatorOrValue;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
operator = operatorOrValue;
|
|
35
|
+
value = maybeValue;
|
|
36
|
+
}
|
|
37
|
+
const clause = {
|
|
38
|
+
type: 'where',
|
|
39
|
+
column,
|
|
40
|
+
operator,
|
|
41
|
+
value,
|
|
42
|
+
};
|
|
43
|
+
this._conditions.push(clause);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/** WHERE column IS NULL */
|
|
47
|
+
whereNull(column) {
|
|
48
|
+
this._conditions.push({
|
|
49
|
+
type: 'where',
|
|
50
|
+
column,
|
|
51
|
+
operator: 'isNull',
|
|
52
|
+
value: null,
|
|
53
|
+
});
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
/** WHERE column IS NOT NULL */
|
|
57
|
+
whereNotNull(column) {
|
|
58
|
+
this._conditions.push({
|
|
59
|
+
type: 'where',
|
|
60
|
+
column,
|
|
61
|
+
operator: 'isNotNull',
|
|
62
|
+
value: null,
|
|
63
|
+
});
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
/** WHERE column IN (...values) */
|
|
67
|
+
whereIn(column, values) {
|
|
68
|
+
this._conditions.push({
|
|
69
|
+
type: 'where',
|
|
70
|
+
column,
|
|
71
|
+
operator: 'in',
|
|
72
|
+
value: values,
|
|
73
|
+
});
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/** WHERE column BETWEEN low AND high */
|
|
77
|
+
whereBetween(column, low, high) {
|
|
78
|
+
this._conditions.push({
|
|
79
|
+
type: 'where',
|
|
80
|
+
column,
|
|
81
|
+
operator: 'between',
|
|
82
|
+
value: [low, high],
|
|
83
|
+
});
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
/** WHERE column LIKE pattern */
|
|
87
|
+
whereLike(column, pattern) {
|
|
88
|
+
this._conditions.push({
|
|
89
|
+
type: 'where',
|
|
90
|
+
column,
|
|
91
|
+
operator: 'like',
|
|
92
|
+
value: pattern,
|
|
93
|
+
});
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
/** Combine conditions with AND */
|
|
97
|
+
and(builder) {
|
|
98
|
+
const sub = new QueryBuilder(this._table);
|
|
99
|
+
builder(sub);
|
|
100
|
+
const andClause = {
|
|
101
|
+
type: 'and',
|
|
102
|
+
conditions: sub._conditions,
|
|
103
|
+
};
|
|
104
|
+
this._conditions.push(andClause);
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
/** Combine conditions with OR */
|
|
108
|
+
or(builder) {
|
|
109
|
+
const sub = new QueryBuilder(this._table);
|
|
110
|
+
builder(sub);
|
|
111
|
+
const orClause = {
|
|
112
|
+
type: 'or',
|
|
113
|
+
conditions: sub._conditions,
|
|
114
|
+
};
|
|
115
|
+
this._conditions.push(orClause);
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
/** Add ORDER BY */
|
|
119
|
+
orderBy(column, order = 'asc') {
|
|
120
|
+
this._orderBy.push({ column, order });
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
/** Set LIMIT */
|
|
124
|
+
limit(n) {
|
|
125
|
+
this._limit = n;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/** Set OFFSET */
|
|
129
|
+
offset(n) {
|
|
130
|
+
this._offset = n;
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
/** Add a JOIN clause */
|
|
134
|
+
join(table, leftColumn, rightColumn) {
|
|
135
|
+
this._joins.push({ table, leftColumn, rightColumn });
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
/** Build the final query descriptor */
|
|
139
|
+
build() {
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
table: this._table,
|
|
142
|
+
conditions: Object.freeze([...this._conditions]),
|
|
143
|
+
orderBy: Object.freeze([...this._orderBy]),
|
|
144
|
+
limit: this._limit,
|
|
145
|
+
offset: this._offset,
|
|
146
|
+
joins: Object.freeze([...this._joins]),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Clone this builder for forking */
|
|
150
|
+
clone() {
|
|
151
|
+
const qb = new QueryBuilder(this._table);
|
|
152
|
+
qb._conditions = [...this._conditions];
|
|
153
|
+
qb._orderBy = [...this._orderBy];
|
|
154
|
+
qb._limit = this._limit;
|
|
155
|
+
qb._offset = this._offset;
|
|
156
|
+
qb._joins = [...this._joins];
|
|
157
|
+
return qb;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.QueryBuilder = QueryBuilder;
|
|
161
|
+
// ─── Convenience factory ───────────────────────────────────────────────────
|
|
162
|
+
function query(table) {
|
|
163
|
+
return new QueryBuilder(table);
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=QueryBuilder.js.map
|