outlet-orm 5.5.3 → 6.5.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 +4 -2
- package/package.json +1 -1
- package/src/Backup/BackupEncryption.js +153 -0
- package/src/Backup/BackupManager.js +422 -0
- package/src/Backup/BackupScheduler.js +175 -0
- package/src/Backup/BackupSocketClient.js +275 -0
- package/src/Backup/BackupSocketServer.js +347 -0
- package/src/Model.js +154 -2
- package/src/QueryBuilder.js +82 -0
- package/src/index.js +15 -1
- package/types/index.d.ts +266 -0
package/types/index.d.ts
CHANGED
|
@@ -153,6 +153,20 @@ declare module 'outlet-orm' {
|
|
|
153
153
|
to: number;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/** Observer interface for model lifecycle events */
|
|
157
|
+
export interface ModelObserver<T extends Model = Model> {
|
|
158
|
+
creating?(model: T): boolean | void | Promise<boolean | void>;
|
|
159
|
+
created?(model: T): void | Promise<void>;
|
|
160
|
+
updating?(model: T): boolean | void | Promise<boolean | void>;
|
|
161
|
+
updated?(model: T): void | Promise<void>;
|
|
162
|
+
saving?(model: T): boolean | void | Promise<boolean | void>;
|
|
163
|
+
saved?(model: T): void | Promise<void>;
|
|
164
|
+
deleting?(model: T): boolean | void | Promise<boolean | void>;
|
|
165
|
+
deleted?(model: T): void | Promise<void>;
|
|
166
|
+
restoring?(model: T): boolean | void | Promise<boolean | void>;
|
|
167
|
+
restored?(model: T): void | Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
export class QueryBuilder<T extends Model> {
|
|
157
171
|
constructor(model: typeof Model);
|
|
158
172
|
|
|
@@ -204,6 +218,12 @@ declare module 'outlet-orm' {
|
|
|
204
218
|
get(): Promise<T[]>;
|
|
205
219
|
first(): Promise<T | null>;
|
|
206
220
|
firstOrFail(): Promise<T>;
|
|
221
|
+
/** Get the first record matching current wheres or create a new one */
|
|
222
|
+
firstOrCreate(values?: Record<string, any>): Promise<T>;
|
|
223
|
+
/** Get the first record matching current wheres or return a new unsaved instance */
|
|
224
|
+
firstOrNew(values?: Record<string, any>): Promise<T>;
|
|
225
|
+
/** Find a record matching current wheres and update it, or create a new one */
|
|
226
|
+
updateOrCreate(values?: Record<string, any>): Promise<T>;
|
|
207
227
|
paginate(page?: number, perPage?: number): Promise<PaginationResult<T>>;
|
|
208
228
|
count(): Promise<number>;
|
|
209
229
|
exists(): Promise<boolean>;
|
|
@@ -214,6 +234,8 @@ declare module 'outlet-orm' {
|
|
|
214
234
|
delete(): Promise<any>;
|
|
215
235
|
increment(column: string, amount?: number): Promise<any>;
|
|
216
236
|
decrement(column: string, amount?: number): Promise<any>;
|
|
237
|
+
/** Lazily iterate over matching records using an async generator */
|
|
238
|
+
cursor(chunkSize?: number): AsyncGenerator<T, void, unknown>;
|
|
217
239
|
|
|
218
240
|
clone(): QueryBuilder<T>;
|
|
219
241
|
}
|
|
@@ -344,6 +366,25 @@ declare module 'outlet-orm' {
|
|
|
344
366
|
static whereNull<T extends Model>(this: new () => T, column: string): QueryBuilder<T>;
|
|
345
367
|
static whereNotNull<T extends Model>(this: new () => T, column: string): QueryBuilder<T>;
|
|
346
368
|
static count(): Promise<number>;
|
|
369
|
+
|
|
370
|
+
// Convenience query methods
|
|
371
|
+
/** Find the first record matching conditions or create a new one */
|
|
372
|
+
static firstOrCreate<T extends Model>(this: new () => T, conditions: Record<string, any>, values?: Record<string, any>): Promise<T>;
|
|
373
|
+
/** Find the first record matching conditions or return a new unsaved instance */
|
|
374
|
+
static firstOrNew<T extends Model>(this: new () => T, conditions: Record<string, any>, values?: Record<string, any>): Promise<T>;
|
|
375
|
+
/** Find a record matching conditions and update it, or create a new one */
|
|
376
|
+
static updateOrCreate<T extends Model>(this: new () => T, conditions: Record<string, any>, values?: Record<string, any>): Promise<T>;
|
|
377
|
+
/** Insert or update multiple records in bulk (ON CONFLICT / ON DUPLICATE KEY) */
|
|
378
|
+
static upsert(rows: Record<string, any>[], uniqueBy: string | string[], update?: string[]): Promise<any>;
|
|
379
|
+
|
|
380
|
+
// Observer
|
|
381
|
+
/** Register an observer class that listens to model lifecycle events */
|
|
382
|
+
static observe(observer: ModelObserver | (new () => ModelObserver)): void;
|
|
383
|
+
|
|
384
|
+
// Cursor / Stream
|
|
385
|
+
/** Lazily iterate over all matching records using an async generator */
|
|
386
|
+
static cursor<T extends Model>(this: new () => T, chunkSize?: number): AsyncGenerator<T, void, unknown>;
|
|
387
|
+
|
|
347
388
|
static with<T extends Model>(this: new () => T, ...relations: string[] | [Record<string, (qb: QueryBuilder<any>) => void> | string[]]): QueryBuilder<T>;
|
|
348
389
|
/** Include hidden attributes in query results */
|
|
349
390
|
static withHidden<T extends Model>(this: new () => T): QueryBuilder<T>;
|
|
@@ -671,4 +712,229 @@ declare module 'outlet-orm' {
|
|
|
671
712
|
run(target?: string | null): Promise<void>;
|
|
672
713
|
runSeeder(seederRef: string): Promise<void>;
|
|
673
714
|
}
|
|
715
|
+
|
|
716
|
+
// ==================== Backup ====================
|
|
717
|
+
|
|
718
|
+
export type BackupFormat = 'sql' | 'json';
|
|
719
|
+
export type BackupType = 'full' | 'partial' | 'journal';
|
|
720
|
+
export type SaltLength = 4 | 5 | 6;
|
|
721
|
+
|
|
722
|
+
/** Encryption options for backup files */
|
|
723
|
+
export interface EncryptionOptions {
|
|
724
|
+
/** Enable AES-256-GCM encryption (default: false) */
|
|
725
|
+
encrypt?: boolean;
|
|
726
|
+
/** Password used for key derivation (required when encrypt=true) */
|
|
727
|
+
encryptionPassword?: string;
|
|
728
|
+
/**
|
|
729
|
+
* Grain de sable (salt) length in characters.
|
|
730
|
+
* Must be 4, 5, or 6. Default: 6.
|
|
731
|
+
*/
|
|
732
|
+
saltLength?: SaltLength;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export interface BackupOptions extends EncryptionOptions {
|
|
736
|
+
/** Override the auto-generated filename */
|
|
737
|
+
filename?: string;
|
|
738
|
+
/** Output format – 'sql' (default) or 'json' */
|
|
739
|
+
format?: BackupFormat;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export interface JournalOptions extends EncryptionOptions {
|
|
743
|
+
/** Override the auto-generated filename */
|
|
744
|
+
filename?: string;
|
|
745
|
+
/** Clear the query log after writing the journal (default: false) */
|
|
746
|
+
flush?: boolean;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export interface RestoreOptions {
|
|
750
|
+
/**
|
|
751
|
+
* Password to decrypt an encrypted backup.
|
|
752
|
+
* Falls back to the BackupManager constructor password.
|
|
753
|
+
*/
|
|
754
|
+
encryptionPassword?: string;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export interface RestoreResult {
|
|
758
|
+
/** Number of SQL statements executed */
|
|
759
|
+
statements: number;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export interface BackupManagerOptions extends EncryptionOptions {
|
|
763
|
+
/** Directory where backup files are written (default: './database/backups') */
|
|
764
|
+
backupPath?: string;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export interface ScheduleConfig extends EncryptionOptions {
|
|
768
|
+
/** Interval between backups in milliseconds (minimum: 1000) */
|
|
769
|
+
intervalMs: number;
|
|
770
|
+
/** Run once immediately when scheduled (default: false) */
|
|
771
|
+
runNow?: boolean;
|
|
772
|
+
/** Table names – required for 'partial' type */
|
|
773
|
+
tables?: string[];
|
|
774
|
+
/** Output format for full/partial backups */
|
|
775
|
+
format?: BackupFormat;
|
|
776
|
+
/** Auto-flush query log after each journal backup */
|
|
777
|
+
flush?: boolean;
|
|
778
|
+
/** Called with the file path after a successful backup */
|
|
779
|
+
onSuccess?: (filePath: string) => void;
|
|
780
|
+
/** Called with the error when a backup fails */
|
|
781
|
+
onError?: (error: Error) => void;
|
|
782
|
+
/** Optional unique job identifier (defaults to "<type>_<timestamp>") */
|
|
783
|
+
name?: string;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export class BackupManager {
|
|
787
|
+
constructor(connection: DatabaseConnection, options?: BackupManagerOptions);
|
|
788
|
+
|
|
789
|
+
/** Full backup – schema + data for every table. */
|
|
790
|
+
full(options?: BackupOptions): Promise<string>;
|
|
791
|
+
/** Partial backup – schema + data for the specified tables only. */
|
|
792
|
+
partial(tables: string[], options?: BackupOptions): Promise<string>;
|
|
793
|
+
/**
|
|
794
|
+
* Transaction-log backup – replayable DML statements from the query log.
|
|
795
|
+
* Requires DatabaseConnection.enableQueryLog() to be called beforehand.
|
|
796
|
+
*/
|
|
797
|
+
journal(options?: JournalOptions): Promise<string>;
|
|
798
|
+
/** Restore a previously created SQL backup file (supports encrypted files). */
|
|
799
|
+
restore(filePath: string, options?: RestoreOptions): Promise<RestoreResult>;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export class BackupScheduler {
|
|
803
|
+
constructor(connection: DatabaseConnection, options?: BackupManagerOptions);
|
|
804
|
+
|
|
805
|
+
schedule(type: BackupType, config: ScheduleConfig): string;
|
|
806
|
+
stop(name: string): void;
|
|
807
|
+
stopAll(): void;
|
|
808
|
+
activeJobs(): string[];
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ==================== Backup Encryption ====================
|
|
812
|
+
|
|
813
|
+
export interface EncryptResult {
|
|
814
|
+
/** Full file content to write to disk */
|
|
815
|
+
encryptedContent: string;
|
|
816
|
+
/** The grain de sable (salt) that was generated */
|
|
817
|
+
salt: string;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export namespace BackupEncryption {
|
|
821
|
+
/**
|
|
822
|
+
* Encrypt a string payload with AES-256-GCM.
|
|
823
|
+
* @param plaintext Content to encrypt
|
|
824
|
+
* @param password Encryption password
|
|
825
|
+
* @param saltLength Grain de sable length (4–6, default 6)
|
|
826
|
+
*/
|
|
827
|
+
function encrypt(plaintext: string, password: string, saltLength?: SaltLength): EncryptResult;
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Decrypt a previously encrypted backup payload.
|
|
831
|
+
* @param encryptedContent Raw file content produced by encrypt()
|
|
832
|
+
* @param password The same password used during encryption
|
|
833
|
+
*/
|
|
834
|
+
function decrypt(encryptedContent: string, password: string): string;
|
|
835
|
+
|
|
836
|
+
/** Return true if the content looks like an outlet-orm encrypted backup. */
|
|
837
|
+
function isEncrypted(content: string): boolean;
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Generate a random alphanumeric salt (grain de sable).
|
|
841
|
+
* @param length 4, 5, or 6 characters
|
|
842
|
+
*/
|
|
843
|
+
function generateSalt(length?: SaltLength): string;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ==================== Backup Socket Server ====================
|
|
847
|
+
|
|
848
|
+
export interface ServerOptions extends BackupManagerOptions {
|
|
849
|
+
/** TCP port to listen on (default: 9119) */
|
|
850
|
+
port?: number;
|
|
851
|
+
/** Host / IP to bind (default: '127.0.0.1') */
|
|
852
|
+
host?: string;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export interface ServerStatus {
|
|
856
|
+
uptime: number;
|
|
857
|
+
jobs: string[];
|
|
858
|
+
clients: number;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export interface ServerAddress {
|
|
862
|
+
host: string;
|
|
863
|
+
port: number;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export class BackupSocketServer {
|
|
867
|
+
constructor(connection: DatabaseConnection, options?: ServerOptions);
|
|
868
|
+
|
|
869
|
+
/** Start the TCP daemon. */
|
|
870
|
+
listen(): Promise<void>;
|
|
871
|
+
/** Gracefully stop the daemon and all scheduled jobs. */
|
|
872
|
+
close(): Promise<void>;
|
|
873
|
+
/** Return the bound address (null before listen). */
|
|
874
|
+
address(): ServerAddress | null;
|
|
875
|
+
|
|
876
|
+
on(event: 'listening', listener: (addr: ServerAddress) => void): this;
|
|
877
|
+
on(event: 'close', listener: () => void): this;
|
|
878
|
+
on(event: 'error', listener: (err: Error) => void): this;
|
|
879
|
+
on(event: 'event', listener: (payload: Record<string, any>) => void): this;
|
|
880
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ==================== Backup Socket Client ====================
|
|
884
|
+
|
|
885
|
+
export interface ClientOptions {
|
|
886
|
+
/** TCP port of the server (default: 9119) */
|
|
887
|
+
port?: number;
|
|
888
|
+
/** Host of the server (default: '127.0.0.1') */
|
|
889
|
+
host?: string;
|
|
890
|
+
/** Timeout in ms waiting for a server reply (default: 30000) */
|
|
891
|
+
timeout?: number;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export interface RunOptions extends EncryptionOptions {
|
|
895
|
+
format? : BackupFormat;
|
|
896
|
+
filename?: string;
|
|
897
|
+
flush? : boolean;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
export class BackupSocketClient {
|
|
901
|
+
constructor(options?: ClientOptions);
|
|
902
|
+
|
|
903
|
+
readonly connected: boolean;
|
|
904
|
+
|
|
905
|
+
connect(): Promise<void>;
|
|
906
|
+
disconnect(): Promise<void>;
|
|
907
|
+
|
|
908
|
+
ping(): Promise<'pong'>;
|
|
909
|
+
status(): Promise<ServerStatus>;
|
|
910
|
+
jobs(): Promise<string[]>;
|
|
911
|
+
|
|
912
|
+
schedule(type: BackupType, config: ScheduleConfig): Promise<string>;
|
|
913
|
+
stop(name: string): Promise<boolean>;
|
|
914
|
+
stopAll(): Promise<boolean>;
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Trigger a one-shot backup immediately.
|
|
918
|
+
* @param tables Required for 'partial' type
|
|
919
|
+
*/
|
|
920
|
+
run(type: BackupType, options?: RunOptions): Promise<string>;
|
|
921
|
+
run(type: 'partial', tables: string[], options?: RunOptions): Promise<string>;
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Restore a previously created backup file on the server.
|
|
925
|
+
* Supports plain SQL and encrypted .enc files.
|
|
926
|
+
* @param filePath Absolute path to the backup file (server-side)
|
|
927
|
+
* @param options Supply encryptionPassword when the file is encrypted
|
|
928
|
+
*/
|
|
929
|
+
restore(filePath: string, options?: RestoreOptions): Promise<RestoreResult>;
|
|
930
|
+
|
|
931
|
+
on(event: 'connect', listener: () => void): this;
|
|
932
|
+
on(event: 'disconnect', listener: () => void): this;
|
|
933
|
+
on(event: 'error', listener: (err: Error) => void): this;
|
|
934
|
+
on(event: 'jobStart', listener: (payload: { name: string; type: string }) => void): this;
|
|
935
|
+
on(event: 'jobDone', listener: (payload: { name: string; type: string; filePath: string }) => void): this;
|
|
936
|
+
on(event: 'jobError', listener: (payload: { name: string; type: string; error: string }) => void): this;
|
|
937
|
+
on(event: 'serverEvent', listener: (payload: Record<string, any>) => void): this;
|
|
938
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
939
|
+
}
|
|
674
940
|
}
|