@platformatic/sqlite-js-stream 0.0.0-stage → 0.1.1
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/CHANGELOG.md +40 -0
- package/LICENSE +202 -0
- package/NOTICE +22 -0
- package/README.md +482 -2
- package/dist/backends/file/index.d.ts +4 -0
- package/dist/backends/file/index.d.ts.map +1 -0
- package/dist/backends/file/index.js +3 -0
- package/dist/backends/file/leaser.d.ts +20 -0
- package/dist/backends/file/leaser.d.ts.map +1 -0
- package/dist/backends/file/leaser.js +123 -0
- package/dist/backends/file/replica-client.d.ts +38 -0
- package/dist/backends/file/replica-client.d.ts.map +1 -0
- package/dist/backends/file/replica-client.js +209 -0
- package/dist/db.d.ts +80 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +484 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/leaser.d.ts +55 -0
- package/dist/leaser.d.ts.map +1 -0
- package/dist/leaser.js +60 -0
- package/dist/ltx/byte-reader.d.ts +18 -0
- package/dist/ltx/byte-reader.d.ts.map +1 -0
- package/dist/ltx/byte-reader.js +38 -0
- package/dist/ltx/bytes.d.ts +20 -0
- package/dist/ltx/bytes.d.ts.map +1 -0
- package/dist/ltx/bytes.js +69 -0
- package/dist/ltx/checksum.d.ts +20 -0
- package/dist/ltx/checksum.d.ts.map +1 -0
- package/dist/ltx/checksum.js +42 -0
- package/dist/ltx/compaction-level.d.ts +58 -0
- package/dist/ltx/compaction-level.d.ts.map +1 -0
- package/dist/ltx/compaction-level.js +115 -0
- package/dist/ltx/compactor.d.ts +33 -0
- package/dist/ltx/compactor.d.ts.map +1 -0
- package/dist/ltx/compactor.js +137 -0
- package/dist/ltx/constants.d.ts +49 -0
- package/dist/ltx/constants.d.ts.map +1 -0
- package/dist/ltx/constants.js +58 -0
- package/dist/ltx/crc64.d.ts +29 -0
- package/dist/ltx/crc64.d.ts.map +1 -0
- package/dist/ltx/crc64.js +57 -0
- package/dist/ltx/decoder.d.ts +55 -0
- package/dist/ltx/decoder.d.ts.map +1 -0
- package/dist/ltx/decoder.js +236 -0
- package/dist/ltx/encoder.d.ts +49 -0
- package/dist/ltx/encoder.d.ts.map +1 -0
- package/dist/ltx/encoder.js +159 -0
- package/dist/ltx/file-info.d.ts +24 -0
- package/dist/ltx/file-info.d.ts.map +1 -0
- package/dist/ltx/file-info.js +74 -0
- package/dist/ltx/filename.d.ts +13 -0
- package/dist/ltx/filename.d.ts.map +1 -0
- package/dist/ltx/filename.js +23 -0
- package/dist/ltx/header.d.ts +17 -0
- package/dist/ltx/header.d.ts.map +1 -0
- package/dist/ltx/header.js +115 -0
- package/dist/ltx/index.d.ts +24 -0
- package/dist/ltx/index.d.ts.map +1 -0
- package/dist/ltx/index.js +25 -0
- package/dist/ltx/lz4.d.ts +41 -0
- package/dist/ltx/lz4.d.ts.map +1 -0
- package/dist/ltx/lz4.js +264 -0
- package/dist/ltx/page-index.d.ts +26 -0
- package/dist/ltx/page-index.d.ts.map +1 -0
- package/dist/ltx/page-index.js +118 -0
- package/dist/ltx/page.d.ts +27 -0
- package/dist/ltx/page.d.ts.map +1 -0
- package/dist/ltx/page.js +66 -0
- package/dist/ltx/sink.d.ts +17 -0
- package/dist/ltx/sink.d.ts.map +1 -0
- package/dist/ltx/sink.js +21 -0
- package/dist/ltx/stream.d.ts +64 -0
- package/dist/ltx/stream.d.ts.map +1 -0
- package/dist/ltx/stream.js +248 -0
- package/dist/ltx/streaming-compactor.d.ts +34 -0
- package/dist/ltx/streaming-compactor.d.ts.map +1 -0
- package/dist/ltx/streaming-compactor.js +140 -0
- package/dist/ltx/trailer.d.ts +12 -0
- package/dist/ltx/trailer.d.ts.map +1 -0
- package/dist/ltx/trailer.js +47 -0
- package/dist/ltx/types.d.ts +79 -0
- package/dist/ltx/types.d.ts.map +1 -0
- package/dist/ltx/types.js +70 -0
- package/dist/replica-client.d.ts +92 -0
- package/dist/replica-client.d.ts.map +1 -0
- package/dist/replica-client.js +122 -0
- package/dist/replica-url.d.ts +20 -0
- package/dist/replica-url.d.ts.map +1 -0
- package/dist/replica-url.js +49 -0
- package/dist/replica.d.ts +379 -0
- package/dist/replica.d.ts.map +1 -0
- package/dist/replica.js +1253 -0
- package/dist/store.d.ts +34 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +60 -0
- package/dist/util/fs.d.ts +28 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +73 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +7 -0
- package/dist/util/stream.d.ts +11 -0
- package/dist/util/stream.d.ts.map +1 -0
- package/dist/util/stream.js +25 -0
- package/dist/wal/checksum.d.ts +17 -0
- package/dist/wal/checksum.d.ts.map +1 -0
- package/dist/wal/checksum.js +30 -0
- package/dist/wal/constants.d.ts +8 -0
- package/dist/wal/constants.d.ts.map +1 -0
- package/dist/wal/constants.js +7 -0
- package/dist/wal/index.d.ts +5 -0
- package/dist/wal/index.d.ts.map +1 -0
- package/dist/wal/index.js +4 -0
- package/dist/wal/reader.d.ts +61 -0
- package/dist/wal/reader.d.ts.map +1 -0
- package/dist/wal/reader.js +207 -0
- package/package.json +68 -3
- package/src/backends/file/index.ts +3 -0
- package/src/backends/file/leaser.ts +145 -0
- package/src/backends/file/replica-client.ts +246 -0
- package/src/db.ts +553 -0
- package/src/errors.ts +74 -0
- package/src/index.ts +26 -0
- package/src/leaser.ts +86 -0
- package/src/ltx/byte-reader.ts +46 -0
- package/src/ltx/bytes.ts +87 -0
- package/src/ltx/checksum.ts +46 -0
- package/src/ltx/compaction-level.ts +135 -0
- package/src/ltx/compactor.ts +167 -0
- package/src/ltx/constants.ts +71 -0
- package/src/ltx/crc64.ts +65 -0
- package/src/ltx/decoder.ts +282 -0
- package/src/ltx/encoder.ts +200 -0
- package/src/ltx/file-info.ts +81 -0
- package/src/ltx/filename.ts +27 -0
- package/src/ltx/header.ts +126 -0
- package/src/ltx/index.ts +27 -0
- package/src/ltx/lz4.ts +279 -0
- package/src/ltx/page-index.ts +140 -0
- package/src/ltx/page.ts +80 -0
- package/src/ltx/sink.ts +33 -0
- package/src/ltx/stream.ts +286 -0
- package/src/ltx/streaming-compactor.ts +169 -0
- package/src/ltx/trailer.ts +58 -0
- package/src/ltx/types.ts +152 -0
- package/src/lz4js.d.ts +27 -0
- package/src/replica-client.ts +222 -0
- package/src/replica-url.ts +72 -0
- package/src/replica.ts +1480 -0
- package/src/store.ts +71 -0
- package/src/util/fs.ts +82 -0
- package/src/util/logger.ts +15 -0
- package/src/util/stream.ts +28 -0
- package/src/wal/checksum.ts +39 -0
- package/src/wal/constants.ts +10 -0
- package/src/wal/index.ts +5 -0
- package/src/wal/reader.ts +264 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReplicaClient interface and shared helpers for interacting with a replica
|
|
3
|
+
* (an LTX destination). Mirrors litestream's `replica_client.go`.
|
|
4
|
+
*
|
|
5
|
+
* The interface is async/iterable oriented rather than Go's iterator/readcloser
|
|
6
|
+
* model, but the semantics match:
|
|
7
|
+
* - ltxFiles returns LTX file metadata for a level, optionally seeking by TXID.
|
|
8
|
+
* - openLtxFile returns a byte stream for a range of an LTX file.
|
|
9
|
+
* - writeLtxFile writes an LTX file, preserving its creation timestamp.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import nodePath from 'node:path';
|
|
13
|
+
import type { Readable } from 'node:stream';
|
|
14
|
+
|
|
15
|
+
import type { FileInfo, Header, PageIndex, TXID } from './ltx/index.ts';
|
|
16
|
+
import {
|
|
17
|
+
decodePageData,
|
|
18
|
+
decodePageIndex,
|
|
19
|
+
formatFilename,
|
|
20
|
+
HeaderSize,
|
|
21
|
+
parseFilename,
|
|
22
|
+
TrailerSize,
|
|
23
|
+
type DecodedPage,
|
|
24
|
+
unmarshalHeader,
|
|
25
|
+
} from './ltx/index.ts';
|
|
26
|
+
import { ByteReader } from './ltx/index.ts';
|
|
27
|
+
import { LTXFormatError, SqliteJsStreamError } from './errors.ts';
|
|
28
|
+
import { readU64BE } from './ltx/index.ts';
|
|
29
|
+
import { readAll } from './util/stream.ts';
|
|
30
|
+
import type { Logger } from './util/logger.ts';
|
|
31
|
+
|
|
32
|
+
/** Sentinel error to stop early iteration in FindLTXFiles. */
|
|
33
|
+
export const ErrStopIter = Symbol('stop iterator');
|
|
34
|
+
|
|
35
|
+
import { SnapshotLevel } from './ltx/constants.ts';
|
|
36
|
+
|
|
37
|
+
/** SnapshotLevel is the LTX level reserved for full snapshots. */
|
|
38
|
+
export { SnapshotLevel };
|
|
39
|
+
|
|
40
|
+
export interface ReplicaClient {
|
|
41
|
+
/** Type returns the type of client (e.g. "file"). */
|
|
42
|
+
type(): string;
|
|
43
|
+
/** Init initializes the replica client connection (idempotent). */
|
|
44
|
+
init(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* ltxFiles returns all LTX files for a level. If seek is non-zero, only files
|
|
47
|
+
* with MinTXID >= seek are returned. useMetadata controls timestamp accuracy.
|
|
48
|
+
*/
|
|
49
|
+
ltxFiles(opts: LTXFilesOptions): AsyncIterable<FileInfo>;
|
|
50
|
+
/**
|
|
51
|
+
* openLtxFile returns a reader over an LTX file range. If size is 0 the range
|
|
52
|
+
* extends to the end of the file. Throws if the file does not exist.
|
|
53
|
+
*/
|
|
54
|
+
openLtxFile(opts: OpenLTXFileOptions): Promise<Readable>;
|
|
55
|
+
/** writeLtxFile writes an LTX file to the replica and returns its metadata. */
|
|
56
|
+
writeLtxFile(
|
|
57
|
+
opts: WriteLTXFileOptions,
|
|
58
|
+
body: AsyncIterable<Uint8Array | string>,
|
|
59
|
+
): Promise<FileInfo>;
|
|
60
|
+
/** deleteLtxFiles deletes one or more LTX files. */
|
|
61
|
+
deleteLtxFiles(infos: FileInfo[]): Promise<void>;
|
|
62
|
+
/** deleteAll deletes all files on the replica. */
|
|
63
|
+
deleteAll(): Promise<void>;
|
|
64
|
+
/** setLogger sets the logger for the client. */
|
|
65
|
+
setLogger(logger: Logger): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LTXFilesOptions {
|
|
69
|
+
level: number;
|
|
70
|
+
seek: TXID;
|
|
71
|
+
useMetadata: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface OpenLTXFileOptions {
|
|
75
|
+
level: number;
|
|
76
|
+
minTXID: TXID;
|
|
77
|
+
maxTXID: TXID;
|
|
78
|
+
offset: number;
|
|
79
|
+
size: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface WriteLTXFileOptions {
|
|
83
|
+
level: number;
|
|
84
|
+
minTXID: TXID;
|
|
85
|
+
maxTXID: TXID;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** LTXDir returns the path to an LTX directory (root/ltx). */
|
|
89
|
+
export function ltxDir(root: string): string {
|
|
90
|
+
return nodePath.join(root, 'ltx');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** LTXLevelDir returns the path to an LTX level directory. */
|
|
94
|
+
export function ltxLevelDir(root: string, level: number): string {
|
|
95
|
+
return nodePath.join(ltxDir(root), String(level));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** LTXFilePath returns the path to a single LTX file. */
|
|
99
|
+
export function ltxFilePath(
|
|
100
|
+
root: string,
|
|
101
|
+
level: number,
|
|
102
|
+
minTXID: TXID,
|
|
103
|
+
maxTXID: TXID,
|
|
104
|
+
): string {
|
|
105
|
+
return nodePath.join(ltxLevelDir(root, level), formatFilename(minTXID, maxTXID));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** LTXError wraps errors reading/writing a specific LTX file. */
|
|
109
|
+
export class LTXError extends SqliteJsStreamError {
|
|
110
|
+
readonly op: string;
|
|
111
|
+
readonly path: string;
|
|
112
|
+
readonly level: number;
|
|
113
|
+
readonly minTXID: TXID;
|
|
114
|
+
readonly maxTXID: TXID;
|
|
115
|
+
readonly cause: unknown;
|
|
116
|
+
|
|
117
|
+
constructor(op: string, path: string, level: number, minTXID: TXID, maxTXID: TXID, err: unknown) {
|
|
118
|
+
super('ERR_LTX', `${op} ltx file: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
this.name = 'LTXError';
|
|
120
|
+
this.op = op;
|
|
121
|
+
this.path = path;
|
|
122
|
+
this.level = level;
|
|
123
|
+
this.minTXID = minTXID;
|
|
124
|
+
this.maxTXID = maxTXID;
|
|
125
|
+
this.cause = err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const DEFAULT_ESTIMATED_PAGE_INDEX_SIZE = 32 * 1024;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* FindLTXFiles returns a list of LTX files on a level matching `filter`.
|
|
133
|
+
* The filter may throw ErrStopIter to end iteration early.
|
|
134
|
+
*/
|
|
135
|
+
export async function findLtxFiles(
|
|
136
|
+
_ctx: unknown,
|
|
137
|
+
client: ReplicaClient,
|
|
138
|
+
level: number,
|
|
139
|
+
useMetadata: boolean,
|
|
140
|
+
filter: (info: FileInfo) => boolean | Promise<boolean>,
|
|
141
|
+
): Promise<FileInfo[]> {
|
|
142
|
+
const out: FileInfo[] = [];
|
|
143
|
+
for await (const info of client.ltxFiles({ level, seek: 0n, useMetadata })) {
|
|
144
|
+
const match = await filter(info);
|
|
145
|
+
if (match) out.push(info);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** FetchLTXHeader reads & returns the LTX header for the given file info. */
|
|
151
|
+
export async function fetchLtxHeader(client: ReplicaClient, info: FileInfo): Promise<Header> {
|
|
152
|
+
const rc = await client.openLtxFile({
|
|
153
|
+
level: info.level,
|
|
154
|
+
minTXID: info.minTXID,
|
|
155
|
+
maxTXID: info.maxTXID,
|
|
156
|
+
offset: 0,
|
|
157
|
+
size: HeaderSize,
|
|
158
|
+
});
|
|
159
|
+
const b = await readAll(rc);
|
|
160
|
+
return unmarshalHeader(b);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** FetchPageIndex decodes the page index from the tail of an LTX file. */
|
|
164
|
+
export async function fetchPageIndex(
|
|
165
|
+
client: ReplicaClient,
|
|
166
|
+
info: FileInfo,
|
|
167
|
+
): Promise<PageIndex> {
|
|
168
|
+
const b = await fetchPageIndexData(client, info);
|
|
169
|
+
const r = new ByteReader(b, 0);
|
|
170
|
+
return decodePageIndex(r, info.level, info.minTXID, info.maxTXID);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Streaming fetchTail for the page index; sizes and reopens as needed. */
|
|
174
|
+
export async function fetchPageIndexData(client: ReplicaClient, info: FileInfo): Promise<Uint8Array> {
|
|
175
|
+
let offset = info.size - DEFAULT_ESTIMATED_PAGE_INDEX_SIZE;
|
|
176
|
+
if (offset < 0) offset = 0;
|
|
177
|
+
|
|
178
|
+
const f = await client.openLtxFile({
|
|
179
|
+
level: info.level,
|
|
180
|
+
minTXID: info.minTXID,
|
|
181
|
+
maxTXID: info.maxTXID,
|
|
182
|
+
offset,
|
|
183
|
+
size: 0,
|
|
184
|
+
});
|
|
185
|
+
const b = await readAll(f);
|
|
186
|
+
|
|
187
|
+
if (b.length < TrailerSize + 8) {
|
|
188
|
+
throw new LTXFormatError(`ltx file too short to contain a page index: ${b.length} bytes`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const size = readU64BE(b, b.length - TrailerSize - 8);
|
|
192
|
+
const relativeStart = b.length - Number(size) - TrailerSize - 8;
|
|
193
|
+
if (relativeStart > 0) {
|
|
194
|
+
return b.subarray(relativeStart);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const f2 = await client.openLtxFile({
|
|
198
|
+
level: info.level,
|
|
199
|
+
minTXID: info.minTXID,
|
|
200
|
+
maxTXID: info.maxTXID,
|
|
201
|
+
offset: info.size - TrailerSize - 8 - Number(size),
|
|
202
|
+
size: 0,
|
|
203
|
+
});
|
|
204
|
+
return readAll(f2);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** FetchPage fetches and decodes a single page frame from an LTX file. */
|
|
208
|
+
export async function fetchPage(
|
|
209
|
+
client: ReplicaClient,
|
|
210
|
+
level: number,
|
|
211
|
+
minTXID: TXID,
|
|
212
|
+
maxTXID: TXID,
|
|
213
|
+
offset: number,
|
|
214
|
+
size: number,
|
|
215
|
+
): Promise<DecodedPage> {
|
|
216
|
+
const f = await client.openLtxFile({ level, minTXID, maxTXID, offset, size });
|
|
217
|
+
const b = await readAll(f);
|
|
218
|
+
return decodePageData(b);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Re-exported for convenience in the file backend.
|
|
222
|
+
export { formatFilename, parseFilename };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replica URL parsing and replica-client factory registration.
|
|
3
|
+
* Mirrors litestream's `replica_url.go` (simplified: file scheme only for now).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReplicaClient } from './replica-client.ts';
|
|
7
|
+
import { ReplicaURLError } from './errors.ts';
|
|
8
|
+
|
|
9
|
+
export type ReplicaClientFactory = (
|
|
10
|
+
scheme: string,
|
|
11
|
+
host: string,
|
|
12
|
+
urlPath: string,
|
|
13
|
+
query: URLSearchParams | null,
|
|
14
|
+
userinfo: string | null,
|
|
15
|
+
) => ReplicaClient;
|
|
16
|
+
|
|
17
|
+
export interface ParsedReplicaURL {
|
|
18
|
+
scheme: string;
|
|
19
|
+
host: string;
|
|
20
|
+
urlPath: string;
|
|
21
|
+
query: URLSearchParams | null;
|
|
22
|
+
userinfo: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const factories = new Map<string, ReplicaClientFactory>();
|
|
26
|
+
|
|
27
|
+
/** RegisterReplicaClientFactory registers a factory for a URL scheme. */
|
|
28
|
+
export function registerReplicaClientFactory(scheme: string, factory: ReplicaClientFactory): void {
|
|
29
|
+
factories.set(scheme, factory);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** NewReplicaClientFromURL creates a ReplicaClient from a URL string. */
|
|
33
|
+
export function newReplicaClientFromURL(rawURL: string): ReplicaClient {
|
|
34
|
+
const parsed = parseReplicaURLWithQuery(rawURL);
|
|
35
|
+
const factory = factories.get(parsed.scheme);
|
|
36
|
+
if (!factory) {
|
|
37
|
+
throw new ReplicaURLError(`unsupported replica URL scheme: ${parsed.scheme}`);
|
|
38
|
+
}
|
|
39
|
+
return factory(parsed.scheme, parsed.host, parsed.urlPath, parsed.query, parsed.userinfo);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** ParseReplicaURLWithQuery parses a replica URL into components. */
|
|
43
|
+
export function parseReplicaURLWithQuery(s: string): ParsedReplicaURL {
|
|
44
|
+
let u: URL;
|
|
45
|
+
try {
|
|
46
|
+
u = new URL(s);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new ReplicaURLError(`parse replica url ${JSON.stringify(s)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const scheme = u.protocol.replace(/:$/, '');
|
|
52
|
+
if (scheme === 'file') {
|
|
53
|
+
return {
|
|
54
|
+
scheme: 'file',
|
|
55
|
+
host: '',
|
|
56
|
+
urlPath: u.pathname,
|
|
57
|
+
query: null,
|
|
58
|
+
userinfo: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (scheme === '') {
|
|
62
|
+
throw new ReplicaURLError(`replica url scheme required: ${s}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
scheme,
|
|
67
|
+
host: u.host,
|
|
68
|
+
urlPath: decodeURIComponent(u.pathname),
|
|
69
|
+
query: u.searchParams,
|
|
70
|
+
userinfo: u.username ? `${u.username}:${u.password}` : null,
|
|
71
|
+
};
|
|
72
|
+
}
|