@koolbase/core 11.4.0 → 12.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/dist/cjs/aggregate.d.ts +115 -0
- package/dist/cjs/aggregate.js +26 -0
- package/dist/cjs/auth-errors.d.ts +8 -0
- package/dist/cjs/auth-errors.js +27 -14
- package/dist/cjs/auth.d.ts +130 -7
- package/dist/cjs/auth.js +252 -10
- package/dist/cjs/database-errors.d.ts +46 -0
- package/dist/cjs/database-errors.js +71 -1
- package/dist/cjs/database.d.ts +23 -0
- package/dist/cjs/database.js +64 -0
- package/dist/cjs/device-metadata.js +7 -1
- package/dist/cjs/fiscal.d.ts +114 -0
- package/dist/cjs/fiscal.js +167 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/types.d.ts +58 -0
- package/dist/esm/aggregate.d.ts +115 -0
- package/dist/esm/aggregate.js +25 -0
- package/dist/esm/auth-errors.d.ts +8 -0
- package/dist/esm/auth-errors.js +25 -13
- package/dist/esm/auth.d.ts +130 -7
- package/dist/esm/auth.js +253 -11
- package/dist/esm/database-errors.d.ts +46 -0
- package/dist/esm/database-errors.js +66 -0
- package/dist/esm/database.d.ts +23 -0
- package/dist/esm/database.js +64 -0
- package/dist/esm/device-metadata.js +7 -1
- package/dist/esm/fiscal.d.ts +114 -0
- package/dist/esm/fiscal.js +159 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/types.d.ts +58 -0
- package/package.json +1 -1
package/dist/cjs/auth.js
CHANGED
|
@@ -133,6 +133,22 @@ class KoolbaseAuth {
|
|
|
133
133
|
return this.authRequest(path, { ...options, includeAuth: true });
|
|
134
134
|
}
|
|
135
135
|
// ─── Internal session lifecycle ─────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Replace the user inside the current session and persist it.
|
|
138
|
+
*
|
|
139
|
+
* A profile update or a fresh fetch changes the user, not the tokens.
|
|
140
|
+
* Without this the new values live only in the returned object: the cached
|
|
141
|
+
* `currentUser` stays stale, anything bound to `onAuthStateChange` never
|
|
142
|
+
* hears about the change, and a reload restores the old name.
|
|
143
|
+
*
|
|
144
|
+
* No session means nobody is signed in, and there is nothing to update —
|
|
145
|
+
* the caller's own request would have failed first.
|
|
146
|
+
*/
|
|
147
|
+
async updateUserAndPersist(user) {
|
|
148
|
+
if (!this.session)
|
|
149
|
+
return;
|
|
150
|
+
await this.setSessionInternal({ ...this.session, user });
|
|
151
|
+
}
|
|
136
152
|
async setSessionInternal(session) {
|
|
137
153
|
this.session = session;
|
|
138
154
|
if (this.storage) {
|
|
@@ -586,6 +602,188 @@ class KoolbaseAuth {
|
|
|
586
602
|
cooldownUntil: body.cooldown_until ? new Date(body.cooldown_until) : null,
|
|
587
603
|
};
|
|
588
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* The signed-in user, fetched fresh from the server.
|
|
607
|
+
*
|
|
608
|
+
* `currentUser` returns what was cached at sign-in; this asks. Use it when
|
|
609
|
+
* something may have changed the user server-side — a verification
|
|
610
|
+
* completing, a profile updated from another device — and persist the
|
|
611
|
+
* result so the cached copy stops being stale.
|
|
612
|
+
*/
|
|
613
|
+
async getCurrentUser() {
|
|
614
|
+
const res = await this.authRequest('/v1/sdk/auth/me', {
|
|
615
|
+
method: 'GET',
|
|
616
|
+
includeAuth: true,
|
|
617
|
+
});
|
|
618
|
+
await this.checkResponse(res);
|
|
619
|
+
const user = this.mapUser(await res.json());
|
|
620
|
+
await this.updateUserAndPersist(user);
|
|
621
|
+
return user;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Update the signed-in user's profile.
|
|
625
|
+
*
|
|
626
|
+
* Only the fields given are sent, so passing one leaves the other
|
|
627
|
+
* untouched. The updated user is persisted, so `currentUser` reflects it
|
|
628
|
+
* without a refetch.
|
|
629
|
+
*/
|
|
630
|
+
async updateProfile(params) {
|
|
631
|
+
const body = {};
|
|
632
|
+
if (params.fullName !== undefined)
|
|
633
|
+
body.full_name = params.fullName;
|
|
634
|
+
if (params.avatarUrl !== undefined)
|
|
635
|
+
body.avatar_url = params.avatarUrl;
|
|
636
|
+
const res = await this.authRequest('/v1/sdk/auth/me', {
|
|
637
|
+
method: 'PATCH',
|
|
638
|
+
body,
|
|
639
|
+
includeAuth: true,
|
|
640
|
+
});
|
|
641
|
+
await this.checkResponse(res);
|
|
642
|
+
const user = this.mapUser(await res.json());
|
|
643
|
+
await this.updateUserAndPersist(user);
|
|
644
|
+
return user;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Change the signed-in user's password.
|
|
648
|
+
*
|
|
649
|
+
* The current password is required: a stolen session should not be enough
|
|
650
|
+
* to lock the real owner out. A wrong one throws
|
|
651
|
+
* CurrentPasswordIncorrectError — the server returns `invalid_password`,
|
|
652
|
+
* which reads like a rejected new password and means the opposite.
|
|
653
|
+
*
|
|
654
|
+
* An account that signed up through Google or Apple has no password to
|
|
655
|
+
* change and gets the same error, deliberately, so a caller cannot probe
|
|
656
|
+
* which sign-in methods an account has.
|
|
657
|
+
*/
|
|
658
|
+
async changePassword(params) {
|
|
659
|
+
if (params.newPassword.length < 8)
|
|
660
|
+
throw new auth_errors_js_1.WeakPasswordError();
|
|
661
|
+
const res = await this.authRequest('/v1/sdk/auth/me/password', {
|
|
662
|
+
method: 'PATCH',
|
|
663
|
+
body: {
|
|
664
|
+
current_password: params.currentPassword,
|
|
665
|
+
new_password: params.newPassword,
|
|
666
|
+
},
|
|
667
|
+
includeAuth: true,
|
|
668
|
+
});
|
|
669
|
+
await this.checkResponse(res);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Delete the signed-in user's account, and sign out.
|
|
673
|
+
*
|
|
674
|
+
* Deletes the caller only — there is no user id to pass, which is the
|
|
675
|
+
* security property. The server removes the auth record and every session
|
|
676
|
+
* on every device, then fires an `auth.user.deleted` trigger.
|
|
677
|
+
*
|
|
678
|
+
* It does NOT delete the account's records in your collections. Koolbase
|
|
679
|
+
* cannot know whether a row should be deleted, anonymised, or kept for a
|
|
680
|
+
* statutory retention period. Subscribe a Function to `auth.user.deleted`
|
|
681
|
+
* and clean up there — it runs server-side, so a tab closing mid-flow
|
|
682
|
+
* cannot strand anything.
|
|
683
|
+
*/
|
|
684
|
+
async deleteAccount() {
|
|
685
|
+
const res = await this.authRequest('/v1/sdk/auth/me', {
|
|
686
|
+
method: 'DELETE',
|
|
687
|
+
includeAuth: true,
|
|
688
|
+
});
|
|
689
|
+
await this.checkResponse(res);
|
|
690
|
+
await this.clearSessionInternal();
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Every session the signed-in user has — a "where you're signed in" list.
|
|
694
|
+
*
|
|
695
|
+
* Newest first is not guaranteed; sort by createdAt if the order matters.
|
|
696
|
+
* The entry with `isCurrent` is this device.
|
|
697
|
+
*/
|
|
698
|
+
async listSessions() {
|
|
699
|
+
const res = await this.authRequest('/v1/sdk/auth/sessions', {
|
|
700
|
+
method: 'GET',
|
|
701
|
+
includeAuth: true,
|
|
702
|
+
});
|
|
703
|
+
await this.checkResponse(res);
|
|
704
|
+
const body = (await res.json());
|
|
705
|
+
return (body.sessions ?? []).map((s) => {
|
|
706
|
+
const r = s;
|
|
707
|
+
return {
|
|
708
|
+
id: String(r.id),
|
|
709
|
+
ip: r.ip,
|
|
710
|
+
userAgent: r.user_agent,
|
|
711
|
+
deviceLabel: r.device_label,
|
|
712
|
+
createdAt: String(r.created_at),
|
|
713
|
+
expiresAt: String(r.expires_at),
|
|
714
|
+
isCurrent: Boolean(r.is_current),
|
|
715
|
+
};
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Sign out one session by id.
|
|
720
|
+
*
|
|
721
|
+
* Revoking the current one ends this session too — the server does not
|
|
722
|
+
* refuse it, and the local session is cleared here so the app does not keep
|
|
723
|
+
* a token the server has dropped. Use logout() when that is what you mean.
|
|
724
|
+
*/
|
|
725
|
+
async revokeSession(sessionId) {
|
|
726
|
+
const res = await this.authRequest(`/v1/sdk/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, { method: 'POST', includeAuth: true });
|
|
727
|
+
await this.checkResponse(res);
|
|
728
|
+
// Revoking your own session leaves a token the server has dropped. The
|
|
729
|
+
// SDK cannot tell from the response which one it was, and re-listing to
|
|
730
|
+
// find out would cost a request on every revoke — so it asks the only
|
|
731
|
+
// party that already knows: the next request. If it was this session,
|
|
732
|
+
// that request 401s and the normal refresh path clears it.
|
|
733
|
+
//
|
|
734
|
+
// A UI listing sessions knows which row is current from isCurrent, and
|
|
735
|
+
// should call logout() rather than revokeSession() for that one, which
|
|
736
|
+
// is both clearer to the user and one fewer round trip.
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Sign out every OTHER session, keeping this one.
|
|
740
|
+
*
|
|
741
|
+
* What "sign out my other devices" means after a lost phone. Returns how
|
|
742
|
+
* many were ended, so an app can say so rather than guess.
|
|
743
|
+
*/
|
|
744
|
+
async revokeAllOtherSessions() {
|
|
745
|
+
const res = await this.authRequest('/v1/sdk/auth/sessions/revoke-all', {
|
|
746
|
+
method: 'POST',
|
|
747
|
+
includeAuth: true,
|
|
748
|
+
});
|
|
749
|
+
await this.checkResponse(res);
|
|
750
|
+
const body = (await res.json().catch(() => ({})));
|
|
751
|
+
return body.revoked_count ?? 0;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* What has happened to this account — sign-ins, failures, lockouts,
|
|
755
|
+
* password changes.
|
|
756
|
+
*
|
|
757
|
+
* For a "recent security activity" screen. The server sanitizes each
|
|
758
|
+
* event, so this carries what that event type is allowed to say and
|
|
759
|
+
* nothing more.
|
|
760
|
+
*
|
|
761
|
+
* @param limit up to 200; the server caps it there.
|
|
762
|
+
*/
|
|
763
|
+
async auditLog(options) {
|
|
764
|
+
const params = new URLSearchParams();
|
|
765
|
+
if (options?.limit !== undefined)
|
|
766
|
+
params.set('limit', String(options.limit));
|
|
767
|
+
if (options?.offset !== undefined)
|
|
768
|
+
params.set('offset', String(options.offset));
|
|
769
|
+
const qs = params.toString();
|
|
770
|
+
const res = await this.authRequest(`/v1/sdk/auth/audit${qs ? `?${qs}` : ''}`, { method: 'GET', includeAuth: true });
|
|
771
|
+
await this.checkResponse(res);
|
|
772
|
+
const body = (await res.json());
|
|
773
|
+
return {
|
|
774
|
+
events: (body.events ?? []).map((e) => ({
|
|
775
|
+
id: String(e.id),
|
|
776
|
+
eventType: String(e.event_type),
|
|
777
|
+
occurredAt: String(e.occurred_at),
|
|
778
|
+
ip: e.ip,
|
|
779
|
+
userAgent: e.user_agent,
|
|
780
|
+
eventData: e.event_data ?? {},
|
|
781
|
+
})),
|
|
782
|
+
total: Number(body.total ?? 0),
|
|
783
|
+
limit: Number(body.limit ?? 0),
|
|
784
|
+
offset: Number(body.offset ?? 0),
|
|
785
|
+
};
|
|
786
|
+
}
|
|
589
787
|
async unlock(token) {
|
|
590
788
|
const res = await this.authRequest('/v1/sdk/auth/unlock', {
|
|
591
789
|
method: 'POST',
|
|
@@ -626,20 +824,54 @@ class KoolbaseAuth {
|
|
|
626
824
|
}
|
|
627
825
|
// ─── OAuth (DEPRECATED — see v1.10.0) ───────────────────────────────────
|
|
628
826
|
/**
|
|
629
|
-
* @deprecated
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
* server endpoints under /v1/sdk/auth/oauth/{apple,google,github}.
|
|
634
|
-
* Use email/password sign-in for now.
|
|
827
|
+
* @deprecated Never implemented; kept only so existing code compiles.
|
|
828
|
+
* Google and Apple sign-in are supported — use signInWithGoogle() or
|
|
829
|
+
* signInWithApple(). This used to say OAuth was not yet shipped and to
|
|
830
|
+
* use email and password instead, which stopped being true long ago.
|
|
635
831
|
*
|
|
636
832
|
* @throws Always throws KoolbaseAuthError('not_implemented').
|
|
637
833
|
*/
|
|
638
834
|
async oauthLogin(_params) {
|
|
639
|
-
throw new auth_errors_js_1.KoolbaseAuthError('
|
|
640
|
-
'
|
|
641
|
-
|
|
642
|
-
|
|
835
|
+
throw new auth_errors_js_1.KoolbaseAuthError('oauthLogin() was never implemented and is kept only so existing ' +
|
|
836
|
+
'code compiles. Use signInWithGoogle() or signInWithApple().', 'not_implemented');
|
|
837
|
+
}
|
|
838
|
+
// ─── Email code ─────────────────────────────────────────────────────────
|
|
839
|
+
/**
|
|
840
|
+
* Emails a six-digit sign-in code.
|
|
841
|
+
*
|
|
842
|
+
* Resolves the same way whether or not the address has an account, so it
|
|
843
|
+
* cannot be used to check who is registered. A new address receives a code
|
|
844
|
+
* only while the project accepts sign-ups; a disabled account receives
|
|
845
|
+
* nothing.
|
|
846
|
+
*
|
|
847
|
+
* @throws EmailCodeDisabledError if the project has switched this off
|
|
848
|
+
* @throws RateLimitError after too many requests for one address
|
|
849
|
+
*/
|
|
850
|
+
async requestEmailCode(params) {
|
|
851
|
+
const res = await this.authRequest('/v1/sdk/auth/email/code', {
|
|
852
|
+
method: 'POST',
|
|
853
|
+
body: { email: params.email },
|
|
854
|
+
});
|
|
855
|
+
if (!res.ok)
|
|
856
|
+
await this.throwTypedError(res, false);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Signs in with a code from requestEmailCode(), storing the session exactly
|
|
860
|
+
* as login() does. An unknown address becomes an account only if the
|
|
861
|
+
* project still accepts sign-ups at this moment. Each code works once and
|
|
862
|
+
* allows three attempts.
|
|
863
|
+
*
|
|
864
|
+
* @throws OtpInvalidError, OtpExpiredError, OtpMaxAttemptsError
|
|
865
|
+
* @throws EmailCodeDisabledError if the project has switched this off
|
|
866
|
+
*/
|
|
867
|
+
async signInWithEmailCode(params) {
|
|
868
|
+
const res = await this.authRequest('/v1/sdk/auth/email/code/verify', {
|
|
869
|
+
method: 'POST',
|
|
870
|
+
body: { email: params.email, code: params.code },
|
|
871
|
+
});
|
|
872
|
+
const session = await this.parseSessionResponse(res, false);
|
|
873
|
+
await this.setSessionInternal(session);
|
|
874
|
+
return session;
|
|
643
875
|
}
|
|
644
876
|
// ─── Phone OTP ──────────────────────────────────────────────────────────
|
|
645
877
|
async sendOtp(params) {
|
|
@@ -832,6 +1064,16 @@ class KoolbaseAuth {
|
|
|
832
1064
|
case 'contact_not_verified':
|
|
833
1065
|
case 'email_not_verified':
|
|
834
1066
|
throw new auth_errors_js_1.ContactNotVerifiedError(msg || undefined);
|
|
1067
|
+
// Sign-in codes. The otp_* codes are shared with phone sign-in; mapped
|
|
1068
|
+
// here, every path that returns them throws the class an app catches.
|
|
1069
|
+
case 'email_code_disabled':
|
|
1070
|
+
throw new auth_errors_js_1.EmailCodeDisabledError();
|
|
1071
|
+
case 'otp_expired':
|
|
1072
|
+
throw new auth_errors_js_1.OtpExpiredError();
|
|
1073
|
+
case 'otp_invalid':
|
|
1074
|
+
throw new auth_errors_js_1.OtpInvalidError();
|
|
1075
|
+
case 'otp_max_attempts':
|
|
1076
|
+
throw new auth_errors_js_1.OtpMaxAttemptsError();
|
|
835
1077
|
// Registration
|
|
836
1078
|
case 'signups_disabled':
|
|
837
1079
|
throw new auth_errors_js_1.SignupsDisabledError(msg || undefined);
|
|
@@ -195,6 +195,52 @@ export declare class KoolbaseConflictError extends KoolbaseDataError {
|
|
|
195
195
|
field?: string;
|
|
196
196
|
constructor(message?: string, field?: string);
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* A reference field points at a record that does not exist, is deleted, or
|
|
200
|
+
* lives in another collection — the server responds 400 `reference_invalid`.
|
|
201
|
+
*
|
|
202
|
+
* Checked when the write commits, so it can surface from an insert, an update,
|
|
203
|
+
* an upsert or a batch. In a batch the whole transaction is refused: nothing
|
|
204
|
+
* in it was written.
|
|
205
|
+
*/
|
|
206
|
+
export declare class KoolbaseReferenceInvalidError extends KoolbaseDataError {
|
|
207
|
+
constructor(message?: string);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A record could not be deleted because live records still reference it, under
|
|
211
|
+
* a reference declared `on_delete: restrict` — 409 `reference_in_use`.
|
|
212
|
+
*
|
|
213
|
+
* Delete the referencing records first, or in the same batch: the check runs at
|
|
214
|
+
* commit, so one batch may delete a parent and its children in any order.
|
|
215
|
+
*/
|
|
216
|
+
export declare class KoolbaseReferenceInUseError extends KoolbaseDataError {
|
|
217
|
+
constructor(message?: string);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A reference could not be declared because existing records already point at
|
|
221
|
+
* records that do not exist — 409 `dangling_references`.
|
|
222
|
+
*
|
|
223
|
+
* `dangling` lists the offending records so they can be repaired; Koolbase
|
|
224
|
+
* never repairs them for you. It is capped at the first 50.
|
|
225
|
+
*/
|
|
226
|
+
export declare class KoolbaseDanglingReferencesError extends KoolbaseDataError {
|
|
227
|
+
dangling: Array<{
|
|
228
|
+
record_id: string;
|
|
229
|
+
value: string;
|
|
230
|
+
}>;
|
|
231
|
+
constructor(message?: string, dangling?: Array<{
|
|
232
|
+
record_id: string;
|
|
233
|
+
value: string;
|
|
234
|
+
}>);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* A collection could not be deleted because another collection has a reference
|
|
238
|
+
* field pointing at it — 409 `collection_referenced`. Remove that reference
|
|
239
|
+
* first.
|
|
240
|
+
*/
|
|
241
|
+
export declare class KoolbaseCollectionReferencedError extends KoolbaseDataError {
|
|
242
|
+
constructor(message?: string);
|
|
243
|
+
}
|
|
198
244
|
/**
|
|
199
245
|
* Thrown when the requested record or collection does not exist — the server
|
|
200
246
|
* responds with 404 and code `not_found` / `record_not_found` /
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.KoolbaseVectorDimensionMismatchError = exports.KoolbaseRateLimitError = exports.KoolbasePermissionError = exports.KoolbaseInsufficientAuthorityError = exports.KoolbaseValidationError = exports.KoolbaseNotFoundError = exports.KoolbaseConflictError = exports.KoolbaseConstraintNotFoundError = exports.KoolbaseConstraintExistsError = exports.KoolbaseAmbiguousMatchError = exports.KoolbaseDuplicateValuesError = exports.KoolbaseBatchFailedError = exports.KoolbaseIdempotencyKeyReusedError = exports.KoolbaseRevisionMismatchError = exports.KoolbaseProviderInvalidError = exports.KoolbaseProviderNotConfiguredError = exports.KoolbaseInvalidEmbeddingConfigError = exports.KoolbaseFieldNotAutoEmbedError = exports.KoolbaseVectorFieldExistsError = exports.KoolbaseStateConflictError = exports.KoolbaseNoChangesError = exports.KoolbaseInvalidBodyError = exports.KoolbaseProjectInvalidError = exports.KoolbaseInvitationInvalidError = exports.KoolbaseSlugTakenError = exports.KoolbaseSeedError = exports.KoolbaseDataError = void 0;
|
|
3
|
+
exports.KoolbaseVectorDimensionMismatchError = exports.KoolbaseRateLimitError = exports.KoolbasePermissionError = exports.KoolbaseInsufficientAuthorityError = exports.KoolbaseValidationError = exports.KoolbaseNotFoundError = exports.KoolbaseCollectionReferencedError = exports.KoolbaseDanglingReferencesError = exports.KoolbaseReferenceInUseError = exports.KoolbaseReferenceInvalidError = exports.KoolbaseConflictError = exports.KoolbaseConstraintNotFoundError = exports.KoolbaseConstraintExistsError = exports.KoolbaseAmbiguousMatchError = exports.KoolbaseDuplicateValuesError = exports.KoolbaseBatchFailedError = exports.KoolbaseIdempotencyKeyReusedError = exports.KoolbaseRevisionMismatchError = exports.KoolbaseProviderInvalidError = exports.KoolbaseProviderNotConfiguredError = exports.KoolbaseInvalidEmbeddingConfigError = exports.KoolbaseFieldNotAutoEmbedError = exports.KoolbaseVectorFieldExistsError = exports.KoolbaseStateConflictError = exports.KoolbaseNoChangesError = exports.KoolbaseInvalidBodyError = exports.KoolbaseProjectInvalidError = exports.KoolbaseInvitationInvalidError = exports.KoolbaseSlugTakenError = exports.KoolbaseSeedError = exports.KoolbaseDataError = void 0;
|
|
4
4
|
exports.koolbaseDataError = koolbaseDataError;
|
|
5
5
|
const errors_js_1 = require("./errors.js");
|
|
6
6
|
/**
|
|
@@ -297,6 +297,66 @@ class KoolbaseConflictError extends KoolbaseDataError {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
exports.KoolbaseConflictError = KoolbaseConflictError;
|
|
300
|
+
/**
|
|
301
|
+
* A reference field points at a record that does not exist, is deleted, or
|
|
302
|
+
* lives in another collection — the server responds 400 `reference_invalid`.
|
|
303
|
+
*
|
|
304
|
+
* Checked when the write commits, so it can surface from an insert, an update,
|
|
305
|
+
* an upsert or a batch. In a batch the whole transaction is refused: nothing
|
|
306
|
+
* in it was written.
|
|
307
|
+
*/
|
|
308
|
+
class KoolbaseReferenceInvalidError extends KoolbaseDataError {
|
|
309
|
+
constructor(message) {
|
|
310
|
+
super(message ?? 'A reference points at a record that does not exist', 'reference_invalid');
|
|
311
|
+
this.name = 'KoolbaseReferenceInvalidError';
|
|
312
|
+
Object.setPrototypeOf(this, KoolbaseReferenceInvalidError.prototype);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
exports.KoolbaseReferenceInvalidError = KoolbaseReferenceInvalidError;
|
|
316
|
+
/**
|
|
317
|
+
* A record could not be deleted because live records still reference it, under
|
|
318
|
+
* a reference declared `on_delete: restrict` — 409 `reference_in_use`.
|
|
319
|
+
*
|
|
320
|
+
* Delete the referencing records first, or in the same batch: the check runs at
|
|
321
|
+
* commit, so one batch may delete a parent and its children in any order.
|
|
322
|
+
*/
|
|
323
|
+
class KoolbaseReferenceInUseError extends KoolbaseDataError {
|
|
324
|
+
constructor(message) {
|
|
325
|
+
super(message ?? 'This record is still referenced by other records', 'reference_in_use');
|
|
326
|
+
this.name = 'KoolbaseReferenceInUseError';
|
|
327
|
+
Object.setPrototypeOf(this, KoolbaseReferenceInUseError.prototype);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
exports.KoolbaseReferenceInUseError = KoolbaseReferenceInUseError;
|
|
331
|
+
/**
|
|
332
|
+
* A reference could not be declared because existing records already point at
|
|
333
|
+
* records that do not exist — 409 `dangling_references`.
|
|
334
|
+
*
|
|
335
|
+
* `dangling` lists the offending records so they can be repaired; Koolbase
|
|
336
|
+
* never repairs them for you. It is capped at the first 50.
|
|
337
|
+
*/
|
|
338
|
+
class KoolbaseDanglingReferencesError extends KoolbaseDataError {
|
|
339
|
+
constructor(message, dangling) {
|
|
340
|
+
super(message ?? 'Existing records point at records that do not exist', 'dangling_references');
|
|
341
|
+
this.dangling = dangling ?? [];
|
|
342
|
+
this.name = 'KoolbaseDanglingReferencesError';
|
|
343
|
+
Object.setPrototypeOf(this, KoolbaseDanglingReferencesError.prototype);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
exports.KoolbaseDanglingReferencesError = KoolbaseDanglingReferencesError;
|
|
347
|
+
/**
|
|
348
|
+
* A collection could not be deleted because another collection has a reference
|
|
349
|
+
* field pointing at it — 409 `collection_referenced`. Remove that reference
|
|
350
|
+
* first.
|
|
351
|
+
*/
|
|
352
|
+
class KoolbaseCollectionReferencedError extends KoolbaseDataError {
|
|
353
|
+
constructor(message) {
|
|
354
|
+
super(message ?? 'Another collection references this one', 'collection_referenced');
|
|
355
|
+
this.name = 'KoolbaseCollectionReferencedError';
|
|
356
|
+
Object.setPrototypeOf(this, KoolbaseCollectionReferencedError.prototype);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
exports.KoolbaseCollectionReferencedError = KoolbaseCollectionReferencedError;
|
|
300
360
|
/**
|
|
301
361
|
* Thrown when the requested record or collection does not exist — the server
|
|
302
362
|
* responds with 404 and code `not_found` / `record_not_found` /
|
|
@@ -427,6 +487,16 @@ function koolbaseDataError(status, body, fallbackMessage = 'Request failed') {
|
|
|
427
487
|
switch (code) {
|
|
428
488
|
case 'unique_violation':
|
|
429
489
|
return attach(new KoolbaseConflictError(message, field));
|
|
490
|
+
case 'reference_invalid':
|
|
491
|
+
return attach(new KoolbaseReferenceInvalidError(message));
|
|
492
|
+
case 'reference_in_use':
|
|
493
|
+
return attach(new KoolbaseReferenceInUseError(message));
|
|
494
|
+
case 'dangling_references': {
|
|
495
|
+
const d = (body?.details ?? {});
|
|
496
|
+
return attach(new KoolbaseDanglingReferencesError(message, d.dangling));
|
|
497
|
+
}
|
|
498
|
+
case 'collection_referenced':
|
|
499
|
+
return attach(new KoolbaseCollectionReferencedError(message));
|
|
430
500
|
case 'plan_limit_reached': {
|
|
431
501
|
const d = (body?.details ?? {});
|
|
432
502
|
return new errors_js_1.KoolbasePlanLimitError(message, d.resource, d.limit, d.plan);
|
package/dist/cjs/database.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KoolbaseAggregateRequest, KoolbaseAggregateResult } from './aggregate.js';
|
|
1
2
|
import { KoolbaseConflict } from './conflict.js';
|
|
2
3
|
import { PendingWrite } from './pending-write.js';
|
|
3
4
|
import { KoolbaseConfig, KoolbaseRecord, QueryOptions, QueryResult, UpsertResult, BatchOp, BatchResult, KoolbaseVector, SemanticSearchResult, SearchMode } from './types.js';
|
|
@@ -127,6 +128,28 @@ export declare class KoolbaseDatabase {
|
|
|
127
128
|
* moment matters. Snapshot, not a live handle; per-user.
|
|
128
129
|
*/
|
|
129
130
|
pendingWrites(): Promise<PendingWrite[]>;
|
|
131
|
+
/**
|
|
132
|
+
* Count and total over a whole collection, with the read rule applied
|
|
133
|
+
* inside the query.
|
|
134
|
+
*
|
|
135
|
+
* Not a paged query you add up yourself: the server aggregates the entire
|
|
136
|
+
* authorized set. And never a bare number — collections are schemaless, so
|
|
137
|
+
* a sum that quietly skipped three malformed rows would be a wrong number
|
|
138
|
+
* that looks right. Check `accounting` for what contributed.
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* const r = await db.aggregate({
|
|
142
|
+
* collection: 'orders',
|
|
143
|
+
* groupBy: { field: 'created_at', bucket: 'month', timezone: 'Africa/Accra' },
|
|
144
|
+
* measures: [{ aggregate: 'sum', field: 'total', as: 'revenue' }],
|
|
145
|
+
* });
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* A calendar bucket requires a timezone and the types enforce it: midnight
|
|
149
|
+
* means nothing without one, and Accra and UTC disagree about which day a
|
|
150
|
+
* 23:30 sale belongs to.
|
|
151
|
+
*/
|
|
152
|
+
aggregate(request: KoolbaseAggregateRequest): Promise<KoolbaseAggregateResult>;
|
|
130
153
|
conflicts(): Promise<KoolbaseConflict[]>;
|
|
131
154
|
/**
|
|
132
155
|
* Resolves by id, reloading the stored conflict first.
|
package/dist/cjs/database.js
CHANGED
|
@@ -424,6 +424,70 @@ class KoolbaseDatabase {
|
|
|
424
424
|
const { pending } = await (0, offline_state_js_1.readOfflineState)(userId);
|
|
425
425
|
return pending.map(pending_write_js_1.toPendingWrite);
|
|
426
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Count and total over a whole collection, with the read rule applied
|
|
429
|
+
* inside the query.
|
|
430
|
+
*
|
|
431
|
+
* Not a paged query you add up yourself: the server aggregates the entire
|
|
432
|
+
* authorized set. And never a bare number — collections are schemaless, so
|
|
433
|
+
* a sum that quietly skipped three malformed rows would be a wrong number
|
|
434
|
+
* that looks right. Check `accounting` for what contributed.
|
|
435
|
+
*
|
|
436
|
+
* ```ts
|
|
437
|
+
* const r = await db.aggregate({
|
|
438
|
+
* collection: 'orders',
|
|
439
|
+
* groupBy: { field: 'created_at', bucket: 'month', timezone: 'Africa/Accra' },
|
|
440
|
+
* measures: [{ aggregate: 'sum', field: 'total', as: 'revenue' }],
|
|
441
|
+
* });
|
|
442
|
+
* ```
|
|
443
|
+
*
|
|
444
|
+
* A calendar bucket requires a timezone and the types enforce it: midnight
|
|
445
|
+
* means nothing without one, and Accra and UTC disagree about which day a
|
|
446
|
+
* 23:30 sale belongs to.
|
|
447
|
+
*/
|
|
448
|
+
async aggregate(request) {
|
|
449
|
+
const raw = await this.request('POST', '/v1/sdk/db/aggregate', {
|
|
450
|
+
collection: request.collection,
|
|
451
|
+
...(request.where?.length ? { where: request.where } : {}),
|
|
452
|
+
...(request.groupBy
|
|
453
|
+
? {
|
|
454
|
+
group_by: {
|
|
455
|
+
field: request.groupBy.field,
|
|
456
|
+
...(request.groupBy.bucket
|
|
457
|
+
? {
|
|
458
|
+
bucket: request.groupBy.bucket,
|
|
459
|
+
timezone: request.groupBy.timezone,
|
|
460
|
+
}
|
|
461
|
+
: {}),
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
: {}),
|
|
465
|
+
measures: request.measures.map((m) => ({
|
|
466
|
+
aggregate: m.aggregate,
|
|
467
|
+
...(m.field ? { field: m.field } : {}),
|
|
468
|
+
as: m.as,
|
|
469
|
+
})),
|
|
470
|
+
});
|
|
471
|
+
const accounting = {};
|
|
472
|
+
for (const [name, a] of Object.entries(raw.accounting ?? {})) {
|
|
473
|
+
accounting[name] = {
|
|
474
|
+
counted: Number(a.counted ?? 0),
|
|
475
|
+
skipped: Number(a.skipped ?? 0),
|
|
476
|
+
skippedReason: a.skipped_reason,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
groups: (raw.groups ?? []).map((g) => ({
|
|
481
|
+
category: String(g.category ?? ''),
|
|
482
|
+
values: g.values ?? {},
|
|
483
|
+
})),
|
|
484
|
+
matched: Number(raw.matched ?? 0),
|
|
485
|
+
accounting,
|
|
486
|
+
// Explicitly boolean: a missing field must read as false, not
|
|
487
|
+
// undefined, or `if (!r.tooManyGroups)` passes on a refused request.
|
|
488
|
+
tooManyGroups: raw.too_many_groups === true,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
427
491
|
async conflicts() {
|
|
428
492
|
const userId = this.requireUserId('the conflict list');
|
|
429
493
|
const { conflicts } = await (0, offline_state_js_1.readOfflineState)(userId);
|
|
@@ -52,7 +52,13 @@ class DeviceMetadata {
|
|
|
52
52
|
if (this.cached)
|
|
53
53
|
return this.cached;
|
|
54
54
|
const platform = (0, platform_js_1.getPlatform)().info.os;
|
|
55
|
-
|
|
55
|
+
// 'unknown' rather than empty, matching appVersion above. A browser
|
|
56
|
+
// outside the handful browserVersion() recognises — Brave, Opera,
|
|
57
|
+
// Samsung Internet, an in-app webview — reports nothing, and an empty
|
|
58
|
+
// header stored against a session is indistinguishable from an SDK that
|
|
59
|
+
// never reported itself at all. 'unknown' says the SDK spoke and could
|
|
60
|
+
// not identify the host, which is a different and more useful fact.
|
|
61
|
+
const platformVersion = (0, platform_js_1.getPlatform)().info.version || 'unknown';
|
|
56
62
|
const deviceLabel = await this.getOrCreateDeviceLabel();
|
|
57
63
|
// No User-Agent: a browser forbids setting it, and the same header set
|
|
58
64
|
// now goes out from both hosts. The platform and version below carry
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { KoolbaseConfig } from './types.js';
|
|
2
|
+
import { KoolbaseError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* The lifecycle status of a fiscal intent.
|
|
5
|
+
*
|
|
6
|
+
* A sale is durably recorded the moment `submit` returns; fiscal numbers and
|
|
7
|
+
* the authority's certification follow this state machine:
|
|
8
|
+
*
|
|
9
|
+
* - `queued` / `submitting` / `retrying` — sealed with allocated numbers;
|
|
10
|
+
* delivery to the authority is in progress.
|
|
11
|
+
* - `fiscalized` — the authority answered; for certifying document kinds the
|
|
12
|
+
* `certification` is present.
|
|
13
|
+
* - `blocked` — a precondition failed BEFORE any fiscal number was consumed
|
|
14
|
+
* (see `blockedReason`). Resubmitting the same `clientRef` after fixing the
|
|
15
|
+
* cause resumes it.
|
|
16
|
+
* - `attention` — the authority rejected the document, or its credential died
|
|
17
|
+
* after sealing. An operator resolves it; the register's chain waits behind
|
|
18
|
+
* it.
|
|
19
|
+
* - `voided` — terminally closed by an operator without fiscalization.
|
|
20
|
+
*
|
|
21
|
+
* `unknown` is the forward-compatible fallback: a status this SDK version has
|
|
22
|
+
* not heard of parses as unknown rather than throwing, so a server that grows
|
|
23
|
+
* a state does not break older clients.
|
|
24
|
+
*/
|
|
25
|
+
export type FiscalStatus = 'created' | 'blocked' | 'queued' | 'submitting' | 'retrying' | 'fiscalized' | 'attention' | 'voided' | 'unknown';
|
|
26
|
+
/**
|
|
27
|
+
* The state of one fiscal intent.
|
|
28
|
+
*
|
|
29
|
+
* Jurisdiction-neutral by design: this SDK never learns a jurisdiction's
|
|
30
|
+
* vocabulary. Payloads travel as objects shaped by the device's adapter, and
|
|
31
|
+
* `certification` comes back as the authority granted it — for Ghana that
|
|
32
|
+
* means `ysdcregsig`, `ysdcrecnum`, `qr_code` and so on, which the SDK
|
|
33
|
+
* passes through without naming.
|
|
34
|
+
*/
|
|
35
|
+
export interface FiscalIntentResult {
|
|
36
|
+
intentId: string;
|
|
37
|
+
status: FiscalStatus;
|
|
38
|
+
/** Why a `blocked` intent stopped, when the server said. */
|
|
39
|
+
blockedReason?: string;
|
|
40
|
+
/** Fiscal numbers allocated at sealing. */
|
|
41
|
+
numbers?: Record<string, number>;
|
|
42
|
+
/** When the intent was sealed and its numbers allocated. */
|
|
43
|
+
sealedAt?: string;
|
|
44
|
+
/**
|
|
45
|
+
* What the authority granted, once `status` is `fiscalized`. This is the
|
|
46
|
+
* receipt-rendering payload: for Ghana, the signature, receipt number and
|
|
47
|
+
* QR verification URL.
|
|
48
|
+
*/
|
|
49
|
+
certification?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/** True once the authority has certified the document. */
|
|
52
|
+
export declare function isFiscalized(r: FiscalIntentResult): boolean;
|
|
53
|
+
/** True while delivery to the authority is still in progress. */
|
|
54
|
+
export declare function isPending(r: FiscalIntentResult): boolean;
|
|
55
|
+
/** A fiscal request that failed. Carries the HTTP status where there was one. */
|
|
56
|
+
export declare class KoolbaseFiscalError extends KoolbaseError {
|
|
57
|
+
readonly statusCode?: number;
|
|
58
|
+
constructor(message: string, statusCode?: number);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Classify a response. Status and body in, result or error out.
|
|
62
|
+
*
|
|
63
|
+
* Exported for testability, and because auth handling belongs with the
|
|
64
|
+
* session hook rather than here — the same split the Flutter SDK makes.
|
|
65
|
+
*/
|
|
66
|
+
export declare function decodeFiscalResponse(status: number, body: string): Promise<FiscalIntentResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Koolbase Fiscal — submitting sales to a tax authority.
|
|
69
|
+
*
|
|
70
|
+
* Jurisdiction-neutral: the SDK moves payloads and reads back whatever the
|
|
71
|
+
* authority certified. Ghana's GRA E-VAT is the first jurisdiction; nothing
|
|
72
|
+
* here names it.
|
|
73
|
+
*/
|
|
74
|
+
export declare class KoolbaseFiscal {
|
|
75
|
+
private config;
|
|
76
|
+
private getToken;
|
|
77
|
+
private onSessionExpired?;
|
|
78
|
+
constructor(config: KoolbaseConfig, getToken: () => Promise<string | null>, onSessionExpired?: () => Promise<void>);
|
|
79
|
+
private buildHeaders;
|
|
80
|
+
private decode;
|
|
81
|
+
/**
|
|
82
|
+
* Submit a sale for fiscalization.
|
|
83
|
+
*
|
|
84
|
+
* The sale is durably recorded the moment this returns; fiscal numbers and
|
|
85
|
+
* the authority's certification follow asynchronously — poll `status()`
|
|
86
|
+
* with the same `clientRef` to watch it.
|
|
87
|
+
*
|
|
88
|
+
* **On timeout, do not resubmit blindly.** The sale may have fiscalized
|
|
89
|
+
* anyway: the request reached the server and the response was lost. Poll
|
|
90
|
+
* `status()` with the same `clientRef` instead, which is idempotent and
|
|
91
|
+
* tells you what actually happened.
|
|
92
|
+
*
|
|
93
|
+
* @param clientRef your own reference for this sale; the idempotency key
|
|
94
|
+
* and the handle you poll and render receipts by.
|
|
95
|
+
*/
|
|
96
|
+
submit(params: {
|
|
97
|
+
deviceId: string;
|
|
98
|
+
clientRef: string;
|
|
99
|
+
payload: Record<string, unknown>;
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
}): Promise<FiscalIntentResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Read an intent's current state by the reference it was submitted under.
|
|
104
|
+
*
|
|
105
|
+
* This is the receipt-rendering read: once the status is `fiscalized`, the
|
|
106
|
+
* `certification` carries what the receipt must show — for Ghana, the
|
|
107
|
+
* signature, receipt number and QR verification URL.
|
|
108
|
+
*/
|
|
109
|
+
status(params: {
|
|
110
|
+
deviceId: string;
|
|
111
|
+
clientRef: string;
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
}): Promise<FiscalIntentResult>;
|
|
114
|
+
}
|