|
1 |
| -import { type BSONSerializeOptions, BSONType, type Document, type Timestamp } from '../../bson'; |
| 1 | +import { |
| 2 | + type BSONSerializeOptions, |
| 3 | + BSONType, |
| 4 | + type Document, |
| 5 | + Long, |
| 6 | + parseToElementsToArray, |
| 7 | + type Timestamp |
| 8 | +} from '../../bson'; |
| 9 | +import { MongoUnexpectedServerResponseError } from '../../error'; |
2 | 10 | import { type ClusterTime } from '../../sdam/common';
|
| 11 | +import { type MongoDBNamespace, ns } from '../../utils'; |
3 | 12 | import { OnDemandDocument } from './on_demand/document';
|
4 | 13 |
|
| 14 | +// eslint-disable-next-line no-restricted-syntax |
| 15 | +const enum BSONElementOffset { |
| 16 | + type = 0, |
| 17 | + nameOffset = 1, |
| 18 | + nameLength = 2, |
| 19 | + offset = 3, |
| 20 | + length = 4 |
| 21 | +} |
| 22 | +/** |
| 23 | + * Accepts a BSON payload and checks for na "ok: 0" element. |
| 24 | + * This utility is intended to prevent calling response class constructors |
| 25 | + * that expect the result to be a success and demand certain properties to exist. |
| 26 | + * |
| 27 | + * For example, a cursor response always expects a cursor embedded document. |
| 28 | + * In order to write the class such that the properties reflect that assertion (non-null) |
| 29 | + * we cannot invoke the subclass constructor if the BSON represents an error. |
| 30 | + * |
| 31 | + * @param bytes - BSON document returned from the server |
| 32 | + */ |
| 33 | +export function isErrorResponse(bson: Uint8Array): boolean { |
| 34 | + const elements = parseToElementsToArray(bson, 0); |
| 35 | + for (let eIdx = 0; eIdx < elements.length; eIdx++) { |
| 36 | + const element = elements[eIdx]; |
| 37 | + |
| 38 | + if (element[BSONElementOffset.nameLength] === 2) { |
| 39 | + const nameOffset = element[BSONElementOffset.nameOffset]; |
| 40 | + |
| 41 | + // 111 == "o", 107 == "k" |
| 42 | + if (bson[nameOffset] === 111 && bson[nameOffset + 1] === 107) { |
| 43 | + const valueOffset = element[BSONElementOffset.offset]; |
| 44 | + const valueLength = element[BSONElementOffset.length]; |
| 45 | + |
| 46 | + // If any byte in the length of the ok number (works for any type) is non zero, |
| 47 | + // then it is considered "ok: 1" |
| 48 | + for (let i = valueOffset; i < valueOffset + valueLength; i++) { |
| 49 | + if (bson[i] !== 0x00) return false; |
| 50 | + } |
| 51 | + |
| 52 | + return true; |
| 53 | + } |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + return true; |
| 58 | +} |
| 59 | + |
5 | 60 | /** @internal */
|
6 | 61 | export type MongoDBResponseConstructor = {
|
7 | 62 | new (bson: Uint8Array, offset?: number, isArray?: boolean): MongoDBResponse;
|
8 | 63 | };
|
9 | 64 |
|
10 | 65 | /** @internal */
|
11 | 66 | export class MongoDBResponse extends OnDemandDocument {
|
| 67 | + static is(value: unknown): value is MongoDBResponse { |
| 68 | + return value instanceof MongoDBResponse; |
| 69 | + } |
| 70 | + |
12 | 71 | // {ok:1}
|
13 | 72 | static empty = new MongoDBResponse(new Uint8Array([13, 0, 0, 0, 16, 111, 107, 0, 1, 0, 0, 0, 0]));
|
14 | 73 |
|
@@ -83,27 +142,96 @@ export class MongoDBResponse extends OnDemandDocument {
|
83 | 142 | return this.clusterTime ?? null;
|
84 | 143 | }
|
85 | 144 |
|
86 |
| - public override toObject(options: BSONSerializeOptions = {}): Record<string, any> { |
| 145 | + public override toObject(options?: BSONSerializeOptions): Record<string, any> { |
87 | 146 | const exactBSONOptions = {
|
88 |
| - useBigInt64: options.useBigInt64, |
89 |
| - promoteLongs: options.promoteLongs, |
90 |
| - promoteValues: options.promoteValues, |
91 |
| - promoteBuffers: options.promoteBuffers, |
92 |
| - bsonRegExp: options.bsonRegExp, |
93 |
| - raw: options.raw ?? false, |
94 |
| - fieldsAsRaw: options.fieldsAsRaw ?? {}, |
| 147 | + useBigInt64: options?.useBigInt64, |
| 148 | + promoteLongs: options?.promoteLongs, |
| 149 | + promoteValues: options?.promoteValues, |
| 150 | + promoteBuffers: options?.promoteBuffers, |
| 151 | + bsonRegExp: options?.bsonRegExp, |
| 152 | + raw: options?.raw ?? false, |
| 153 | + fieldsAsRaw: options?.fieldsAsRaw ?? {}, |
95 | 154 | validation: this.parseBsonSerializationOptions(options)
|
96 | 155 | };
|
97 | 156 | return super.toObject(exactBSONOptions);
|
98 | 157 | }
|
99 | 158 |
|
100 |
| - private parseBsonSerializationOptions({ enableUtf8Validation }: BSONSerializeOptions): { |
| 159 | + private parseBsonSerializationOptions(options?: { enableUtf8Validation?: boolean }): { |
101 | 160 | utf8: { writeErrors: false } | false;
|
102 | 161 | } {
|
| 162 | + const enableUtf8Validation = options?.enableUtf8Validation; |
103 | 163 | if (enableUtf8Validation === false) {
|
104 | 164 | return { utf8: false };
|
105 | 165 | }
|
106 |
| - |
107 | 166 | return { utf8: { writeErrors: false } };
|
108 | 167 | }
|
109 | 168 | }
|
| 169 | + |
| 170 | +/** @internal */ |
| 171 | +export class CursorResponse extends MongoDBResponse { |
| 172 | + /** |
| 173 | + * This supports a feature of the FindCursor. |
| 174 | + * It is an optimization to avoid an extra getMore when the limit has been reached |
| 175 | + */ |
| 176 | + static emptyGetMore = { id: new Long(0), length: 0, shift: () => null }; |
| 177 | + |
| 178 | + static override is(value: unknown): value is CursorResponse { |
| 179 | + return value instanceof CursorResponse || value === CursorResponse.emptyGetMore; |
| 180 | + } |
| 181 | + |
| 182 | + public id: Long; |
| 183 | + public ns: MongoDBNamespace | null = null; |
| 184 | + public batchSize = 0; |
| 185 | + |
| 186 | + private batch: OnDemandDocument; |
| 187 | + private iterated = 0; |
| 188 | + |
| 189 | + constructor(bytes: Uint8Array, offset?: number, isArray?: boolean) { |
| 190 | + super(bytes, offset, isArray); |
| 191 | + |
| 192 | + const cursor = this.get('cursor', BSONType.object, true); |
| 193 | + |
| 194 | + const id = cursor.get('id', BSONType.long, true); |
| 195 | + this.id = new Long(Number(id & 0xffff_ffffn), Number((id >> 32n) & 0xffff_ffffn)); |
| 196 | + |
| 197 | + const namespace = cursor.get('ns', BSONType.string); |
| 198 | + if (namespace != null) this.ns = ns(namespace); |
| 199 | + |
| 200 | + if (cursor.has('firstBatch')) this.batch = cursor.get('firstBatch', BSONType.array, true); |
| 201 | + else if (cursor.has('nextBatch')) this.batch = cursor.get('nextBatch', BSONType.array, true); |
| 202 | + else throw new MongoUnexpectedServerResponseError('Cursor document did not contain a batch'); |
| 203 | + |
| 204 | + this.batchSize = this.batch.size(); |
| 205 | + } |
| 206 | + |
| 207 | + get length() { |
| 208 | + return Math.max(this.batchSize - this.iterated, 0); |
| 209 | + } |
| 210 | + |
| 211 | + shift(options?: BSONSerializeOptions): any { |
| 212 | + if (this.iterated >= this.batchSize) { |
| 213 | + return null; |
| 214 | + } |
| 215 | + |
| 216 | + const result = this.batch.get(this.iterated, BSONType.object, true) ?? null; |
| 217 | + this.iterated += 1; |
| 218 | + |
| 219 | + if (options?.raw) { |
| 220 | + return result.toBytes(); |
| 221 | + } else { |
| 222 | + return result.toObject(options); |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + clear() { |
| 227 | + this.iterated = this.batchSize; |
| 228 | + } |
| 229 | + |
| 230 | + pushMany() { |
| 231 | + throw new Error('pushMany Unsupported method'); |
| 232 | + } |
| 233 | + |
| 234 | + push() { |
| 235 | + throw new Error('push Unsupported method'); |
| 236 | + } |
| 237 | +} |
0 commit comments