diff --git a/docs/index.rst b/docs/index.rst index bd20cb928..fa3de5a68 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -138,7 +138,7 @@ app: $ npm init -y # initialize a new npm project $ npm i edgedb $ npm i -D typescript @types/node @edgedb/generate tsx - $ npx tsc --init # initialize basic a basic TypeScript project + $ npx tsc --init # initialize a basic TypeScript project Client ^^^^^^ diff --git a/packages/driver/buildDeno.ts b/packages/driver/buildDeno.ts index 38363991a..34a9faaf0 100644 --- a/packages/driver/buildDeno.ts +++ b/packages/driver/buildDeno.ts @@ -12,10 +12,11 @@ await run({ destDir: "../deno", destEntriesToClean: ["_src", "mod.ts"], sourceFilter: (path) => { - return !/cli\.mts$/.test(path); + return !path.endsWith("cli.mts"); }, pathRewriteRules: [ { match: /^src\/index.node.ts$/, replace: "mod.ts" }, + { match: /^src\/deno.json$/, replace: "deno.json" }, { match: /^src\//, replace: "_src/" }, ], injectImports: [ @@ -28,7 +29,7 @@ await run({ from: "src/globals.deno.ts", }, ], -}).then(async () => +}).then(() => run({ sourceDir: "./test", destDir: "../deno/test", diff --git a/packages/driver/src/baseConn.ts b/packages/driver/src/baseConn.ts index d101f96bd..a3526d53d 100644 --- a/packages/driver/src/baseConn.ts +++ b/packages/driver/src/baseConn.ts @@ -45,7 +45,7 @@ import LRU from "./primitives/lru"; import type { SerializedSessionState } from "./options"; import { Session } from "./options"; -export const PROTO_VER: ProtocolVersion = [1, 0]; +export const PROTO_VER: ProtocolVersion = [2, 0]; export const PROTO_VER_MIN: ProtocolVersion = [0, 9]; enum TransactionStatus { diff --git a/packages/driver/src/codecs/array.ts b/packages/driver/src/codecs/array.ts index d727a3da6..45473309d 100644 --- a/packages/driver/src/codecs/array.ts +++ b/packages/driver/src/codecs/array.ts @@ -27,11 +27,18 @@ import { NamedTupleCodec } from "./namedtuple"; export class ArrayCodec extends Codec implements ICodec { private subCodec: ICodec; private len: number; - - constructor(tid: uuid, subCodec: ICodec, len: number) { + public typeName: string | null; + + constructor( + tid: uuid, + typeName: string | null, + subCodec: ICodec, + len: number, + ) { super(tid); this.subCodec = subCodec; this.len = len; + this.typeName = typeName; } encode(buf: WriteBuffer, obj: any): void { diff --git a/packages/driver/src/codecs/codecs.ts b/packages/driver/src/codecs/codecs.ts index 33c8289b3..ecbdbe1d5 100644 --- a/packages/driver/src/codecs/codecs.ts +++ b/packages/driver/src/codecs/codecs.ts @@ -80,14 +80,14 @@ export const INVALID_CODEC = new NullCodec(INVALID_CODEC_ID); function registerScalarCodec( typename: string, - type: new (tid: uuid) => ICodec, + type: new (tid: uuid, typename: string | null) => ICodec, ): void { const id = KNOWN_TYPENAMES.get(typename); if (id == null) { throw new InternalClientError("unknown type name"); } - SCALAR_CODECS.set(id, new type(id)); + SCALAR_CODECS.set(id, new type(id, typename)); } registerScalarCodec("std::int16", Int16Codec); diff --git a/packages/driver/src/codecs/enum.ts b/packages/driver/src/codecs/enum.ts index 72faf317b..ecd0365de 100644 --- a/packages/driver/src/codecs/enum.ts +++ b/packages/driver/src/codecs/enum.ts @@ -22,8 +22,13 @@ import { StrCodec } from "./text"; export class EnumCodec extends StrCodec implements ICodec { readonly values: string[]; - constructor(tid: uuid, derivedFromTid: uuid | null = null, values: string[]) { - super(tid, derivedFromTid); + constructor( + tid: uuid, + typeName: string | null, + derivedFromTid: uuid | null = null, + values: string[], + ) { + super(tid, typeName, derivedFromTid); this.values = values; } } diff --git a/packages/driver/src/codecs/ifaces.ts b/packages/driver/src/codecs/ifaces.ts index d1989cd3d..fa1bc0f8b 100644 --- a/packages/driver/src/codecs/ifaces.ts +++ b/packages/driver/src/codecs/ifaces.ts @@ -70,9 +70,14 @@ export abstract class ScalarCodec extends Codec { private derivedFromTid: uuid | null = null; private typeName: string | null = null; - constructor(tid: uuid, derivedFromTid: uuid | null = null) { + constructor( + tid: uuid, + typeName: string | null, + derivedFromTid: uuid | null = null, + ) { super(tid); this.derivedFromTid = derivedFromTid; + this.typeName = typeName; } /** @internal */ @@ -80,9 +85,9 @@ export abstract class ScalarCodec extends Codec { this.typeName = typeName; } - derive(tid: uuid): Codec { + derive(tid: uuid, typeName: string | null): Codec { const self = this.constructor; - return new (self as any)(tid, this.tid) as Codec; + return new (self as any)(tid, this.tid, typeName) as Codec; } getSubcodecs(): ICodec[] { diff --git a/packages/driver/src/codecs/namedtuple.ts b/packages/driver/src/codecs/namedtuple.ts index 2335754da..f4a7cafdd 100644 --- a/packages/driver/src/codecs/namedtuple.ts +++ b/packages/driver/src/codecs/namedtuple.ts @@ -32,12 +32,19 @@ export class NamedTupleCodec extends Codec implements ICodec, IArgsCodec { private subCodecs: ICodec[]; private names: string[]; private namesSet: Set; - - constructor(tid: uuid, codecs: ICodec[], names: string[]) { + public typeName: string | null; + + constructor( + tid: uuid, + typeName: string | null, + codecs: ICodec[], + names: string[], + ) { super(tid); this.subCodecs = codecs; this.names = names; this.namesSet = new Set(names); + this.typeName = typeName; } encode(buf: WriteBuffer, object: any): void { diff --git a/packages/driver/src/codecs/range.ts b/packages/driver/src/codecs/range.ts index f95733579..58eac4255 100644 --- a/packages/driver/src/codecs/range.ts +++ b/packages/driver/src/codecs/range.ts @@ -97,10 +97,12 @@ export class RangeCodec extends Codec implements ICodec { readonly tsModule = "edgedb"; private subCodec: ICodec; + readonly typeName: string | null; - constructor(tid: uuid, subCodec: ICodec) { + constructor(tid: uuid, typeName: string | null, subCodec: ICodec) { super(tid); this.subCodec = subCodec; + this.typeName = typeName; } encode(buf: WriteBuffer, obj: any) { @@ -125,10 +127,12 @@ export class MultiRangeCodec extends Codec implements ICodec { readonly tsModule = "edgedb"; private subCodec: ICodec; + public typeName: string | null; - constructor(tid: uuid, subCodec: ICodec) { + constructor(tid: uuid, typeName: string | null, subCodec: ICodec) { super(tid); this.subCodec = subCodec; + this.typeName = typeName; } encode(buf: WriteBuffer, obj: any): void { diff --git a/packages/driver/src/codecs/registry.ts b/packages/driver/src/codecs/registry.ts index 70e2b7a8d..490622b13 100644 --- a/packages/driver/src/codecs/registry.ts +++ b/packages/driver/src/codecs/registry.ts @@ -49,6 +49,8 @@ const CTYPE_ARRAY = 6; const CTYPE_ENUM = 7; const CTYPE_INPUT_SHAPE = 8; const CTYPE_RANGE = 9; +const CTYPE_OBJECT = 10; +const CTYPE_COMPOUND = 11; const CTYPE_MULTIRANGE = 12; export interface CustomCodecSpec { @@ -82,7 +84,7 @@ export class CodecsRegistry { if (int64_bigint) { this.customScalarCodecs.set( INT64_TYPEID, - new numbers.Int64BigintCodec(INT64_TYPEID), + new numbers.Int64BigintCodec(INT64_TYPEID, "std::int64"), ); } else { this.customScalarCodecs.delete(INT64_TYPEID); @@ -91,7 +93,7 @@ export class CodecsRegistry { if (datetime_localDatetime) { this.customScalarCodecs.set( DATETIME_TYPEID, - new datecodecs.LocalDateTimeCodec(DATETIME_TYPEID), + new datecodecs.LocalDateTimeCodec(DATETIME_TYPEID, "std::datetime"), ); } else { this.customScalarCodecs.delete(DATETIME_TYPEID); @@ -100,7 +102,7 @@ export class CodecsRegistry { if (json_string) { this.customScalarCodecs.set( JSON_TYPEID, - new JSONStringCodec(JSON_TYPEID), + new JSONStringCodec(JSON_TYPEID, "std::json"), ); } else { this.customScalarCodecs.delete(JSON_TYPEID); @@ -138,7 +140,15 @@ export class CodecsRegistry { let codec: ICodec | null = null; while (frb.length) { - codec = this._buildCodec(frb, codecsList, protocolVersion); + if (versionGreaterThanOrEqual(protocolVersion, [2, 0])) { + const descLen = frb.readInt32(); + const descBuf = ReadBuffer.alloc(); + frb.sliceInto(descBuf, descLen); + codec = this._buildCodec(descBuf, codecsList, protocolVersion, true); + descBuf.finish("unexpected trailing data in type descriptor buffer"); + } else { + codec = this._buildCodec(frb, codecsList, protocolVersion, false); + } if (codec == null) { // An annotation; ignore. continue; @@ -158,6 +168,7 @@ export class CodecsRegistry { frb: ReadBuffer, cl: ICodec[], protocolVersion: ProtocolVersion, + isProtoV2: boolean, ): ICodec | null { const t = frb.readUInt8(); const tid = frb.readUUID(); @@ -170,6 +181,10 @@ export class CodecsRegistry { if (res != null) { // We have a codec for this "tid"; advance the buffer // so that we can process the next codec. + if (isProtoV2) { + frb.discard(frb.length); + return res; + } switch (t) { case CTYPE_SET: { @@ -294,6 +309,11 @@ export class CodecsRegistry { case CTYPE_SHAPE: case CTYPE_INPUT_SHAPE: { + if (t === CTYPE_SHAPE && isProtoV2) { + const _isEphemeralFreeShape = frb.readBoolean(); + const _objTypePos = frb.readUInt16(); + } + const els = frb.readUInt16(); const codecs: ICodec[] = new Array(els); const names: string[] = new Array(els); @@ -325,6 +345,11 @@ export class CodecsRegistry { names[i] = name; flags[i] = flag!; cards[i] = card!; + + if (t === CTYPE_SHAPE && isProtoV2) { + const sourceTypePos = frb.readUInt16(); + const _sourceType = cl[sourceTypePos]; + } } res = @@ -347,23 +372,83 @@ export class CodecsRegistry { } case CTYPE_SCALAR: { - const pos = frb.readUInt16(); - res = cl[pos]; - if (res == null) { - throw new ProtocolError( - "could not build scalar codec: missing a codec for base scalar", - ); - } - if (!(res instanceof ScalarCodec)) { - throw new ProtocolError( - "could not build scalar codec: base scalar has a non-scalar codec", - ); + if (isProtoV2) { + const typeName = frb.readString(); + const _isSchemaDefined = frb.readBoolean(); + + const ancestorCount = frb.readUInt16(); + const ancestors: ICodec[] = []; + for (let i = 0; i < ancestorCount; i++) { + const ancestorPos = frb.readUInt16(); + const ancestorCodec = cl[ancestorPos]; + if (ancestorCodec == null) { + throw new ProtocolError( + "could not build scalar codec: missing a codec for base scalar", + ); + } + + if (!(ancestorCodec instanceof ScalarCodec)) { + throw new ProtocolError( + `a scalar codec expected for base scalar type, ` + + `got ${ancestorCodec}`, + ); + } + ancestors.push(ancestorCodec); + } + + if (ancestorCount === 0) { + res = this.customScalarCodecs.get(tid) ?? SCALAR_CODECS.get(tid); + if (res == null) { + if (KNOWN_TYPES.has(tid)) { + throw new InternalClientError( + `no JS codec for ${KNOWN_TYPES.get(tid)}`, + ); + } + + throw new InternalClientError( + `no JS codec for the type with ID ${tid}`, + ); + } + } else { + const baseCodec = ancestors[ancestors.length - 1]; + if (!(baseCodec instanceof ScalarCodec)) { + throw new ProtocolError( + `a scalar codec expected for base scalar type, ` + + `got ${baseCodec}`, + ); + } + res = baseCodec.derive(tid, typeName) as ICodec; + } + } else { + const pos = frb.readUInt16(); + res = cl[pos]; + if (res == null) { + throw new ProtocolError( + "could not build scalar codec: missing a codec for base scalar", + ); + } + if (!(res instanceof ScalarCodec)) { + throw new ProtocolError( + "could not build scalar codec: base scalar has a non-scalar codec", + ); + } + res = res.derive(tid, null) as ICodec; } - res = res.derive(tid) as ICodec; break; } case CTYPE_ARRAY: { + let typeName: string | null = null; + if (isProtoV2) { + typeName = frb.readString(); + const _isSchemaDefined = frb.readBoolean(); + const ancestorCount = frb.readUInt16(); + for (let i = 0; i < ancestorCount; i++) { + const ancestorPos = frb.readUInt16(); + const _ancestorCodec = cl[ancestorPos]; + } + } + const pos = frb.readUInt16(); const els = frb.readUInt16(); if (els !== 1) { @@ -378,11 +463,22 @@ export class CodecsRegistry { "could not build array codec: missing subcodec", ); } - res = new ArrayCodec(tid, subCodec, dimLen); + res = new ArrayCodec(tid, typeName, subCodec, dimLen); break; } case CTYPE_TUPLE: { + let typeName: string | null = null; + if (isProtoV2) { + typeName = frb.readString(); + const _isSchemaDefined = frb.readBoolean(); + const ancestorCount = frb.readUInt16(); + for (let i = 0; i < ancestorCount; i++) { + const ancestorPos = frb.readUInt16(); + const _ancestorCodec = cl[ancestorPos]; + } + } + const els = frb.readUInt16(); if (els === 0) { res = EMPTY_TUPLE_CODEC; @@ -398,12 +494,23 @@ export class CodecsRegistry { } codecs[i] = subCodec; } - res = new TupleCodec(tid, codecs); + res = new TupleCodec(tid, typeName, codecs); } break; } case CTYPE_NAMEDTUPLE: { + let typeName: string | null = null; + if (isProtoV2) { + typeName = frb.readString(); + const _isSchemaDefined = frb.readBoolean(); + const ancestorCount = frb.readUInt16(); + for (let i = 0; i < ancestorCount; i++) { + const ancestorPos = frb.readUInt16(); + const _ancestorCodec = cl[ancestorPos]; + } + } + const els = frb.readUInt16(); const codecs = new Array(els); const names = new Array(els); @@ -419,11 +526,21 @@ export class CodecsRegistry { } codecs[i] = subCodec; } - res = new NamedTupleCodec(tid, codecs, names); + res = new NamedTupleCodec(tid, typeName, codecs, names); break; } case CTYPE_ENUM: { + let typeName: string | null = null; + if (isProtoV2) { + typeName = frb.readString(); + const _isSchemaDefined = frb.readBoolean(); + const ancestorCount = frb.readUInt16(); + for (let i = 0; i < ancestorCount; i++) { + const ancestorPos = frb.readUInt16(); + const _ancestorCodec = cl[ancestorPos]; + } + } /* There's no way to customize ordering in JS, so we simply ignore that information and unpack enums into simple strings. @@ -433,11 +550,22 @@ export class CodecsRegistry { for (let i = 0; i < els; i++) { values.push(frb.readString()); } - res = new EnumCodec(tid, null, values); + res = new EnumCodec(tid, typeName, null, values); break; } case CTYPE_RANGE: { + let typeName: string | null = null; + if (isProtoV2) { + typeName = frb.readString(); + const _isSchemaDefined = frb.readBoolean(); + const ancestorCount = frb.readUInt16(); + for (let i = 0; i < ancestorCount; i++) { + const ancestorPos = frb.readUInt16(); + const _ancestorCodec = cl[ancestorPos]; + } + } + const pos = frb.readUInt16(); const subCodec = cl[pos]; if (subCodec == null) { @@ -445,11 +573,35 @@ export class CodecsRegistry { "could not build range codec: missing subcodec", ); } - res = new RangeCodec(tid, subCodec); + res = new RangeCodec(tid, typeName, subCodec); + break; + } + + case CTYPE_OBJECT: { + // Ignore + frb.discard(frb.length); + res = NULL_CODEC; + break; + } + + case CTYPE_COMPOUND: { + // Ignore + frb.discard(frb.length); + res = NULL_CODEC; break; } case CTYPE_MULTIRANGE: { + let typeName: string | null = null; + if (isProtoV2) { + typeName = frb.readString(); + const _isSchemaDefined = frb.readBoolean(); + const ancestorCount = frb.readUInt16(); + for (let i = 0; i < ancestorCount; i++) { + const ancestorPos = frb.readUInt16(); + const _ancestorCodec = cl[ancestorPos]; + } + } const pos = frb.readUInt16(); const subCodec = cl[pos]; if (subCodec == null) { @@ -457,7 +609,7 @@ export class CodecsRegistry { "could not build range codec: missing subcodec", ); } - res = new MultiRangeCodec(tid, subCodec); + res = new MultiRangeCodec(tid, typeName, subCodec); break; } } diff --git a/packages/driver/src/codecs/tuple.ts b/packages/driver/src/codecs/tuple.ts index 249761c48..d7b82f709 100644 --- a/packages/driver/src/codecs/tuple.ts +++ b/packages/driver/src/codecs/tuple.ts @@ -30,10 +30,12 @@ import { export class TupleCodec extends Codec implements ICodec, IArgsCodec { private subCodecs: ICodec[]; + public typeName: string | null; - constructor(tid: uuid, codecs: ICodec[]) { + constructor(tid: uuid, typeName: string | null, codecs: ICodec[]) { super(tid); this.subCodecs = codecs; + this.typeName = typeName; } encode(buf: WriteBuffer, object: any, allowNull = false): void { diff --git a/packages/driver/src/conUtils.ts b/packages/driver/src/conUtils.ts index ec7822146..a9e074584 100644 --- a/packages/driver/src/conUtils.ts +++ b/packages/driver/src/conUtils.ts @@ -68,6 +68,7 @@ export interface NormalizedConnectConfig extends PartiallyNormalizedConfig { logging: boolean; } +// explicit config export interface ConnectConfig { dsn?: string; instanceName?: string; @@ -84,6 +85,7 @@ export interface ConnectConfig { tlsCA?: string; tlsCAFile?: string; tlsSecurity?: TlsSecurity; + tlsServerName?: string; timeout?: number; waitUntilAvailable?: Duration | number; @@ -124,6 +126,7 @@ type ConnectConfigParams = | "cloudProfile" | "tlsCAData" | "tlsSecurity" + | "tlsServerName" | "waitUntilAvailable"; export type ResolvedConnectConfigReadonly = Readonly< @@ -165,6 +168,9 @@ export class ResolvedConnectConfig { _tlsSecurity: TlsSecurity | null = null; _tlsSecuritySource: string | null = null; + _tlsServerName: string | null = null; + _tlsServerNameSource: string | null = null; + _waitUntilAvailable: number | null = null; _waitUntilAvailableSource: string | null = null; @@ -180,6 +186,7 @@ export class ResolvedConnectConfig { this.setSecretKey = this.setSecretKey.bind(this); this.setTlsCAData = this.setTlsCAData.bind(this); this.setTlsCAFile = this.setTlsCAFile.bind(this); + this.setTlsServerName = this.setTlsServerName.bind(this); this.setTlsSecurity = this.setTlsSecurity.bind(this); this.setWaitUntilAvailable = this.setWaitUntilAvailable.bind(this); } @@ -280,6 +287,10 @@ export class ResolvedConnectConfig { ); } + setTlsServerName(serverName: string | null, source: string): boolean { + return this._setParam("tlsServerName", serverName, source, validateHost); + } + setTlsSecurity(tlsSecurity: string | null, source: string): boolean { return this._setParam( "tlsSecurity", @@ -375,6 +386,10 @@ export class ResolvedConnectConfig { return this._cloudProfile ?? "default"; } + get tlsServerName(): string | undefined { + return this._tlsServerName ?? undefined; + } + get tlsSecurity(): Exclude { return this._tlsSecurity && this._tlsSecurity !== "default" ? this._tlsSecurity @@ -431,6 +446,12 @@ export class ResolvedConnectConfig { this._tlsSecurity, this._tlsSecuritySource, ); + outputLine( + "tlsServerName", + this.tlsServerName, + this._tlsServerName, + this._tlsServerNameSource, + ); outputLine( "waitUntilAvailable", this.waitUntilAvailable, @@ -542,6 +563,7 @@ async function parseConnectDsnAndArgs( cloudProfile: getEnv("EDGEDB_CLOUD_PROFILE"), tlsCA: config.tlsCA, tlsCAFile: config.tlsCAFile, + tlsServerName: config.tlsServerName, tlsSecurity: config.tlsSecurity, serverSettings: config.serverSettings, waitUntilAvailable: config.waitUntilAvailable, @@ -565,6 +587,7 @@ async function parseConnectDsnAndArgs( tlsCA: `'tlsCA' option`, tlsCAFile: `'tlsCAFile' option`, tlsSecurity: `'tlsSecurity' option`, + tlsServerName: `tlsServerName option`, serverSettings: `'serverSettings' option`, waitUntilAvailable: `'waitUntilAvailable' option`, }, @@ -602,6 +625,7 @@ async function parseConnectDsnAndArgs( secretKey: getEnv("EDGEDB_SECRET_KEY"), tlsCA: getEnv("EDGEDB_TLS_CA"), tlsCAFile: getEnv("EDGEDB_TLS_CA_FILE"), + tlsServerName: getEnv("EDGEDB_TLS_SERVER_NAME"), tlsSecurity: getEnv("EDGEDB_CLIENT_TLS_SECURITY"), waitUntilAvailable: getEnv("EDGEDB_WAIT_UNTIL_AVAILABLE"), }, @@ -619,6 +643,7 @@ async function parseConnectDsnAndArgs( secretKey: `'EDGEDB_SECRET_KEY' environment variable`, tlsCA: `'EDGEDB_TLS_CA' environment variable`, tlsCAFile: `'EDGEDB_TLS_CA_FILE' environment variable`, + tlsServerName: `EDGEDB_TLS_SERVER_NAME environment variable`, tlsSecurity: `'EDGEDB_CLIENT_TLS_SECURITY' environment variable`, waitUntilAvailable: `'EDGEDB_WAIT_UNTIL_AVAILABLE' environment variable`, }, @@ -654,7 +679,7 @@ async function parseConnectDsnAndArgs( .catch(() => null); if (instName !== null) { - const [cloudProfile, database] = await Promise.all([ + const [cloudProfile, _database, branch] = await Promise.all([ serverUtils .readFileUtf8(stashDir, "cloud-profile") .then((name) => name.trim()) @@ -663,15 +688,32 @@ async function parseConnectDsnAndArgs( .readFileUtf8(stashDir, "database") .then((name) => name.trim()) .catch(() => undefined), + serverUtils + .readFileUtf8(stashDir, "branch") + .then((name) => name.trim()) + .catch(() => undefined), ]); + let database = _database; + + if (database !== undefined && branch !== undefined) { + if (database !== branch) { + throw new InterfaceError( + "Both database and branch exist in the config dir and don't match.", + ); + } else { + database = undefined; + } + } + await resolveConfigOptions( resolvedConfig, - { instanceName: instName, cloudProfile, database }, + { instanceName: instName, cloudProfile, database, branch }, { instanceName: `project linked instance ('${instName}')`, cloudProfile: `project defined cloud instance ('${cloudProfile}')`, database: `project default database`, + branch: `project default branch`, }, "", serverUtils, @@ -710,6 +752,7 @@ interface ResolveConfigOptionsConfig { cloudProfile: string; tlsCA: string; tlsCAFile: string; + tlsServerName: string; tlsSecurity: string; serverSettings: { [key: string]: string }; waitUntilAvailable: number | string | Duration; @@ -785,6 +828,11 @@ async function resolveConfigOptions< sources.tlsCAFile!, readFile, )) || anyOptionsUsed; + anyOptionsUsed = + resolvedConfig.setTlsServerName( + config.tlsServerName ?? null, + sources.tlsServerName!, + ) || anyOptionsUsed; anyOptionsUsed = resolvedConfig.setTlsSecurity( config.tlsSecurity ?? null, @@ -880,7 +928,6 @@ async function resolveConfigOptions< } creds = await readCredentialsFile(credentialsFile, serverUtils!); } - resolvedConfig.setHost(creds.host ?? null, source); resolvedConfig.setPort(creds.port ?? null, source); if (creds.database != null) { @@ -1087,6 +1134,13 @@ async function parseDSNIntoConfig( config.setTlsCAFile(val, _source, readFile), ); + await handleDSNPart( + "tls_server_name", + null, + config._tlsServerName, + config.setTlsServerName, + ); + await handleDSNPart( "tls_security", null, diff --git a/packages/driver/src/deno.json b/packages/driver/src/deno.json new file mode 100644 index 000000000..75446423b --- /dev/null +++ b/packages/driver/src/deno.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "noUnusedLocals": false + } +} diff --git a/packages/driver/src/fetchConn.ts b/packages/driver/src/fetchConn.ts index 7fe201b19..84ef322b4 100644 --- a/packages/driver/src/fetchConn.ts +++ b/packages/driver/src/fetchConn.ts @@ -42,6 +42,7 @@ import Event from "./primitives/event"; import { type AuthenticatedFetch, getAuthenticatedFetch } from "./utils"; const PROTO_MIME = `application/x.edgedb.v_${PROTO_VER[0]}_${PROTO_VER[1]}.binary'`; +const PROTO_MIME_RE = /application\/x\.edgedb\.v_(\d+)_(\d+)\.binary/; const STUDIO_CAPABILITIES = (RESTRICTED_CAPABILITIES | @@ -105,9 +106,14 @@ class BaseFetchConnection extends BaseRawConnection { ); } + const contentType = resp.headers.get("content-type"); + const matchProtoVer = contentType?.match(PROTO_MIME_RE); + if (matchProtoVer) { + this.protocolVersion = [+matchProtoVer[1], +matchProtoVer[2]]; + } + const respData = await resp.arrayBuffer(); const buf = new Uint8Array(respData); - try { this.buffer.feed(buf); } catch (e: any) { diff --git a/packages/driver/src/primitives/buffer.ts b/packages/driver/src/primitives/buffer.ts index 3b86653bd..3341ea791 100644 --- a/packages/driver/src/primitives/buffer.ts +++ b/packages/driver/src/primitives/buffer.ts @@ -882,9 +882,9 @@ export class ReadBuffer { return this.len - this.pos; } - finish(): void { + finish(message?: string): void { if (this.len !== this.pos) { - throw new BufferError("unexpected trailing data in buffer"); + throw new BufferError(message ?? "unexpected trailing data in buffer"); } } @@ -1005,6 +1005,10 @@ export class ReadBuffer { return ret; } + readBoolean(): boolean { + return this.readUInt8() !== 0; + } + readBuffer(size: number): Uint8Array { if (this.pos + size > this.len) { throw new BufferError("buffer overread"); diff --git a/packages/driver/src/rawConn.ts b/packages/driver/src/rawConn.ts index 2f5d95a52..c7852b383 100644 --- a/packages/driver/src/rawConn.ts +++ b/packages/driver/src/rawConn.ts @@ -59,7 +59,7 @@ function getTlsOptions(config: ResolvedConnectConfig): tls.ConnectionOptions { if (!isIPAddress) { // XXX Deno doesn't support this and that means it won't // work with EdgeDB Cloud. - tlsOptions.servername = config.address[0]; + tlsOptions.servername = config.tlsServerName || config.address[0]; } _tlsOptions.set(config, tlsOptions); diff --git a/packages/driver/test/connection-config.test.ts b/packages/driver/test/connection-config.test.ts index 0213b88b2..fbff1d49a 100644 --- a/packages/driver/test/connection-config.test.ts +++ b/packages/driver/test/connection-config.test.ts @@ -192,6 +192,7 @@ interface ConnectionResult { password: string | null; tlsCAData: string | null; tlsSecurity: boolean; + tlsServerName: string | null; serverSettings: { [key: string]: string }; waitUntilAvailable: string; } @@ -254,6 +255,7 @@ async function runConnectionTest(testcase: ConnectionTestCase): Promise { user: connectionParams.user, password: connectionParams.password ?? null, secretKey: connectionParams.secretKey ?? null, + tlsServerName: connectionParams.tlsServerName ?? null, tlsCAData: connectionParams._tlsCAData, tlsSecurity: connectionParams.tlsSecurity, serverSettings: connectionParams.serverSettings, @@ -456,6 +458,7 @@ test("logging, inProject, fromProject, fromEnv", async () => { password: null, tlsCAData: null, tlsSecurity: "strict", + tlsServerName: null, serverSettings: {}, }; @@ -608,6 +611,7 @@ test("logging, inProject, fromProject, fromEnv", async () => { user: connectionParams.user, password: connectionParams.password ?? null, tlsCAData: connectionParams._tlsCAData, + tlsServerName: connectionParams.tlsServerName ?? null, tlsSecurity: connectionParams.tlsSecurity, serverSettings: connectionParams.serverSettings, }).toEqual(testcase.result); diff --git a/packages/driver/test/datetime.test.ts b/packages/driver/test/datetime.test.ts index b267e06f9..18b46a045 100644 --- a/packages/driver/test/datetime.test.ts +++ b/packages/driver/test/datetime.test.ts @@ -16,7 +16,10 @@ import { ReadBuffer, WriteBuffer } from "../src/primitives/buffer"; // Tests adapted from https://github.com/edgedb/edgedb-rust/blob/master/edgedb-protocol/tests/datetime_chrono.rs test("datetime", () => { - const codec = new DateTimeCodec(KNOWN_TYPENAMES.get("std::datetime")!); + const codec = new DateTimeCodec( + KNOWN_TYPENAMES.get("std::datetime")!, + "std::datetime", + ); const tests: [string, string][] = [ ["252455615999999999", "+010000-01-01T00:00:00.000Z"], // maximum @@ -55,6 +58,7 @@ test("datetime", () => { test("local_datetime", () => { const codec = new LocalDateTimeCodec( KNOWN_TYPENAMES.get("cal::local_datetime")!, + "cal::local_datetime", ); const tests: [string, string, string][] = [ @@ -181,7 +185,10 @@ test("local_datetime", () => { }); test("local_time", () => { - const codec = new LocalTimeCodec(KNOWN_TYPENAMES.get("cal::local_time")!); + const codec = new LocalTimeCodec( + KNOWN_TYPENAMES.get("cal::local_time")!, + "cal::local_time", + ); const tests: [string, string][] = [ ["00:00:00.000000000", "0"], // minimum @@ -211,7 +218,10 @@ test("local_time", () => { }); test("duration", () => { - const codec = new DurationCodec(KNOWN_TYPENAMES.get("std::duration")!); + const codec = new DurationCodec( + KNOWN_TYPENAMES.get("std::duration")!, + "std::duration", + ); const tests: [string, string][] = [ ["PT0S", "0"], // "Zero" diff --git a/packages/driver/test/shared-client-testcases b/packages/driver/test/shared-client-testcases index 3ba0df7fd..071f60430 160000 --- a/packages/driver/test/shared-client-testcases +++ b/packages/driver/test/shared-client-testcases @@ -1 +1 @@ -Subproject commit 3ba0df7fde1f252a40476ae665018336e7900f55 +Subproject commit 071f60430207e331b847e1dcc10cba19c5b5f2f4 diff --git a/packages/driver/tsconfig.json b/packages/driver/tsconfig.json index 508864bbe..fa65cc784 100644 --- a/packages/driver/tsconfig.json +++ b/packages/driver/tsconfig.json @@ -2,7 +2,8 @@ "extends": "@repo/tsconfig/base.json", "compilerOptions": { "outDir": "./dist", - "downlevelIteration": true + "downlevelIteration": true, + "noUnusedLocals": false }, "include": ["src/**/*"], "exclude": ["src/cli.mts", "src/**/*.deno.ts"]