@richardmcquiston01/error-log-adapter 0.2.0 → 0.4.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/index.cjs +566 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +373 -1
- package/dist/index.d.ts +373 -1
- package/dist/index.mjs +557 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -2
package/dist/index.mjs
CHANGED
|
@@ -628,17 +628,574 @@ var JsonFileErrorAdapter = class {
|
|
|
628
628
|
return [];
|
|
629
629
|
}
|
|
630
630
|
};
|
|
631
|
+
|
|
632
|
+
// src/adapters/sql-error-adapter.ts
|
|
633
|
+
var DEFAULT_TABLE_NAME2 = "error_log";
|
|
634
|
+
var MAX_TABLE_NAME_LENGTH = 47;
|
|
635
|
+
var IDENTIFIER_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
636
|
+
function assertValidTableName2(tableName, adapterName) {
|
|
637
|
+
if (!IDENTIFIER_PATTERN2.test(tableName)) {
|
|
638
|
+
throw new RangeError(
|
|
639
|
+
`${adapterName}: invalid table name "${tableName}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
if (tableName.length > MAX_TABLE_NAME_LENGTH) {
|
|
643
|
+
throw new RangeError(
|
|
644
|
+
`${adapterName}: table name "${tableName}" is ${tableName.length} characters long, but the limit is ${MAX_TABLE_NAME_LENGTH}.`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function quoteIdentifier(dialect, identifier) {
|
|
649
|
+
return dialect === "postgres" ? `"${identifier}"` : `\`${identifier}\``;
|
|
650
|
+
}
|
|
651
|
+
function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME2) {
|
|
652
|
+
assertValidTableName2(tableName, "buildErrorLogTableDdl");
|
|
653
|
+
const table = quoteIdentifier(dialect, tableName);
|
|
654
|
+
const index = quoteIdentifier(
|
|
655
|
+
dialect,
|
|
656
|
+
`${tableName}_occurred_at_idx`
|
|
657
|
+
);
|
|
658
|
+
if (dialect === "postgres") {
|
|
659
|
+
return [
|
|
660
|
+
`CREATE TABLE IF NOT EXISTS ${table} (id UUID PRIMARY KEY, code TEXT NOT NULL, message TEXT NOT NULL, cause JSONB, source TEXT, occurred_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`,
|
|
661
|
+
`CREATE INDEX IF NOT EXISTS ${index} ON ${table} (occurred_at)`
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
return [
|
|
665
|
+
`CREATE TABLE IF NOT EXISTS ${table} (id CHAR(36) NOT NULL PRIMARY KEY, code VARCHAR(255) NOT NULL, message TEXT NOT NULL, cause JSON NULL, source VARCHAR(255) NULL, occurred_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), INDEX ${index} (occurred_at))`
|
|
666
|
+
];
|
|
667
|
+
}
|
|
668
|
+
function toMysqlDateTime(date) {
|
|
669
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
670
|
+
}
|
|
671
|
+
var SqlErrorAdapter = class {
|
|
672
|
+
constructor(client, dialect, adapterName, options) {
|
|
673
|
+
this.client = client;
|
|
674
|
+
this.dialect = dialect;
|
|
675
|
+
this.adapterName = adapterName;
|
|
676
|
+
this.tableReady = null;
|
|
677
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME2;
|
|
678
|
+
assertValidTableName2(this.tableName, adapterName);
|
|
679
|
+
this.createTable = options.createTable ?? true;
|
|
680
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
681
|
+
}
|
|
682
|
+
async log(entry) {
|
|
683
|
+
try {
|
|
684
|
+
const serialized = serializeEntry(entry);
|
|
685
|
+
const occurredAt = new Date(serialized.timestamp);
|
|
686
|
+
if (Number.isNaN(occurredAt.getTime())) {
|
|
687
|
+
throw new RangeError(
|
|
688
|
+
`timestamp "${serialized.timestamp}" is not a valid ISO 8601 date`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const params = [
|
|
692
|
+
crypto.randomUUID(),
|
|
693
|
+
serialized.code,
|
|
694
|
+
serialized.message,
|
|
695
|
+
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
696
|
+
serialized.source ?? null,
|
|
697
|
+
this.dialect === "postgres" ? occurredAt.toISOString() : toMysqlDateTime(occurredAt)
|
|
698
|
+
];
|
|
699
|
+
await this.ensureTable();
|
|
700
|
+
await this.client.query(this.insertSql(), params);
|
|
701
|
+
return { ok: true };
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.error(`${this.prefix} ${this.adapterName}:`, error);
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
error: `${this.adapterName} failed to write to table "${this.tableName}": ${describeError(error)}`
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
insertSql() {
|
|
711
|
+
const placeholders = this.dialect === "postgres" ? "$1, $2, $3, $4, $5, $6" : "?, ?, ?, ?, ?, ?";
|
|
712
|
+
return `INSERT INTO ${quoteIdentifier(this.dialect, this.tableName)} (id, code, message, cause, source, occurred_at) VALUES (${placeholders})`;
|
|
713
|
+
}
|
|
714
|
+
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
715
|
+
ensureTable() {
|
|
716
|
+
if (!this.createTable) {
|
|
717
|
+
return Promise.resolve();
|
|
718
|
+
}
|
|
719
|
+
if (this.tableReady === null) {
|
|
720
|
+
this.tableReady = this.runCreateStatements().catch(
|
|
721
|
+
(error) => {
|
|
722
|
+
this.tableReady = null;
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
return this.tableReady;
|
|
728
|
+
}
|
|
729
|
+
async runCreateStatements() {
|
|
730
|
+
for (const sql of buildErrorLogTableDdl(this.dialect, this.tableName)) {
|
|
731
|
+
await this.client.query(sql, []);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
var PostgresErrorAdapter = class extends SqlErrorAdapter {
|
|
736
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier of 47 characters or fewer. */
|
|
737
|
+
constructor(client, options = {}) {
|
|
738
|
+
super(client, "postgres", "PostgresErrorAdapter", options);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
var MySqlErrorAdapter = class extends SqlErrorAdapter {
|
|
742
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier of 47 characters or fewer. */
|
|
743
|
+
constructor(client, options = {}) {
|
|
744
|
+
super(client, "mysql", "MySqlErrorAdapter", options);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/adapters/redis-error-adapter.ts
|
|
749
|
+
var DEFAULT_REDIS_KEY = "error_log";
|
|
750
|
+
var RedisErrorAdapter = class {
|
|
751
|
+
/** @throws {RangeError} if `key` is empty, `maxEntries` is not a positive integer, or `mode` / `side` is unknown. */
|
|
752
|
+
constructor(client, options = {}) {
|
|
753
|
+
this.client = client;
|
|
754
|
+
this.mode = options.mode ?? "list";
|
|
755
|
+
this.key = options.key ?? DEFAULT_REDIS_KEY;
|
|
756
|
+
this.maxEntries = options.maxEntries;
|
|
757
|
+
this.side = options.side ?? "right";
|
|
758
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
759
|
+
if (this.mode !== "list" && this.mode !== "stream") {
|
|
760
|
+
throw new RangeError(
|
|
761
|
+
`RedisErrorAdapter: mode must be "list" or "stream", received "${String(options.mode)}".`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
if (this.side !== "left" && this.side !== "right") {
|
|
765
|
+
throw new RangeError(
|
|
766
|
+
`RedisErrorAdapter: side must be "left" or "right", received "${String(options.side)}".`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
if (this.key === "") {
|
|
770
|
+
throw new RangeError("RedisErrorAdapter: key must not be empty.");
|
|
771
|
+
}
|
|
772
|
+
if (this.maxEntries !== void 0 && (!Number.isInteger(this.maxEntries) || this.maxEntries < 1)) {
|
|
773
|
+
throw new RangeError(
|
|
774
|
+
`RedisErrorAdapter: maxEntries must be a positive integer, received ${String(this.maxEntries)}.`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async log(entry) {
|
|
779
|
+
try {
|
|
780
|
+
const record = { id: crypto.randomUUID(), ...serializeEntry(entry) };
|
|
781
|
+
if (this.mode === "stream") {
|
|
782
|
+
await this.client.sendCommand(this.buildStreamCommand(record));
|
|
783
|
+
} else {
|
|
784
|
+
await this.client.sendCommand([
|
|
785
|
+
this.side === "left" ? "LPUSH" : "RPUSH",
|
|
786
|
+
this.key,
|
|
787
|
+
JSON.stringify(record)
|
|
788
|
+
]);
|
|
789
|
+
await this.trimList();
|
|
790
|
+
}
|
|
791
|
+
return { ok: true };
|
|
792
|
+
} catch (error) {
|
|
793
|
+
console.error(`${this.prefix} RedisErrorAdapter:`, error);
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
error: `RedisErrorAdapter failed to write to ${this.mode} "${this.key}": ${describeError(error)}`
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
buildStreamCommand(record) {
|
|
801
|
+
const command = ["XADD", this.key];
|
|
802
|
+
if (this.maxEntries !== void 0) {
|
|
803
|
+
command.push("MAXLEN", "=", String(this.maxEntries));
|
|
804
|
+
}
|
|
805
|
+
command.push(
|
|
806
|
+
"*",
|
|
807
|
+
"id",
|
|
808
|
+
record.id,
|
|
809
|
+
"code",
|
|
810
|
+
record.code,
|
|
811
|
+
"message",
|
|
812
|
+
record.message,
|
|
813
|
+
"timestamp",
|
|
814
|
+
record.timestamp
|
|
815
|
+
);
|
|
816
|
+
if (record.cause !== void 0) {
|
|
817
|
+
command.push("cause", JSON.stringify(record.cause));
|
|
818
|
+
}
|
|
819
|
+
if (record.source !== void 0) {
|
|
820
|
+
command.push("source", record.source);
|
|
821
|
+
}
|
|
822
|
+
return command;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Drops the oldest list entries beyond `maxEntries`. The entry is already
|
|
826
|
+
* stored by now, so a trim failure is reported but does not fail the call;
|
|
827
|
+
* the next successful write trims the list again.
|
|
828
|
+
*/
|
|
829
|
+
async trimList() {
|
|
830
|
+
if (this.maxEntries === void 0) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const keep = this.maxEntries;
|
|
834
|
+
const [start, stop] = this.side === "right" ? [String(-keep), "-1"] : ["0", String(keep - 1)];
|
|
835
|
+
try {
|
|
836
|
+
await this.client.sendCommand(["LTRIM", this.key, start, stop]);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
console.error(
|
|
839
|
+
`${this.prefix} RedisErrorAdapter: stored the entry but could not trim list "${this.key}" to ${keep} entries: ${describeError(error)}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// src/adapters/s3-error-adapter.ts
|
|
846
|
+
var DEFAULT_S3_PREFIX = "error_log/";
|
|
847
|
+
var BUFFER_LIMIT_MULTIPLIER = 10;
|
|
848
|
+
function datePath(date) {
|
|
849
|
+
const compact = date.toISOString().replace(/[-:.]/g, "");
|
|
850
|
+
return `${compact.slice(0, 4)}/${compact.slice(4, 6)}/${compact.slice(6, 8)}/${compact}`;
|
|
851
|
+
}
|
|
852
|
+
function normalizePrefix(prefix) {
|
|
853
|
+
const trimmed = prefix.replace(/^\/+/, "");
|
|
854
|
+
return trimmed === "" || trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
855
|
+
}
|
|
856
|
+
var S3ErrorAdapter = class {
|
|
857
|
+
/** @throws {RangeError} if `bucket` is empty or the batch options are invalid. */
|
|
858
|
+
constructor(client, bucket, options = {}) {
|
|
859
|
+
this.client = client;
|
|
860
|
+
this.bucket = bucket;
|
|
861
|
+
this.buffer = [];
|
|
862
|
+
this.timer = null;
|
|
863
|
+
this.flushQueue = Promise.resolve();
|
|
864
|
+
this.keyPrefix = normalizePrefix(options.keyPrefix ?? DEFAULT_S3_PREFIX);
|
|
865
|
+
this.batch = options.batch;
|
|
866
|
+
this.contentType = options.contentType ?? (this.batch === void 0 ? "application/json" : "application/x-ndjson");
|
|
867
|
+
this.logPrefix = options.prefix ?? "[error-log-adapter]";
|
|
868
|
+
if (bucket === "") {
|
|
869
|
+
throw new RangeError("S3ErrorAdapter: bucket must not be empty.");
|
|
870
|
+
}
|
|
871
|
+
if (this.batch !== void 0) {
|
|
872
|
+
if (!Number.isInteger(this.batch.batchSize) || this.batch.batchSize < 1) {
|
|
873
|
+
throw new RangeError(
|
|
874
|
+
`S3ErrorAdapter: batch.batchSize must be a positive integer, received ${String(this.batch.batchSize)}.`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
if (this.batch.maxWaitMs !== void 0 && (!Number.isFinite(this.batch.maxWaitMs) || this.batch.maxWaitMs <= 0)) {
|
|
878
|
+
throw new RangeError(
|
|
879
|
+
`S3ErrorAdapter: batch.maxWaitMs must be a positive number, received ${String(this.batch.maxWaitMs)}.`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async log(entry) {
|
|
885
|
+
try {
|
|
886
|
+
const record = {
|
|
887
|
+
id: crypto.randomUUID(),
|
|
888
|
+
...serializeEntry(entry)
|
|
889
|
+
};
|
|
890
|
+
const occurredAt = new Date(record.timestamp);
|
|
891
|
+
if (Number.isNaN(occurredAt.getTime())) {
|
|
892
|
+
throw new RangeError(
|
|
893
|
+
`timestamp "${record.timestamp}" is not a valid ISO 8601 date`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
if (this.batch === void 0) {
|
|
897
|
+
const key = `${this.keyPrefix}${datePath(occurredAt)}-${record.id}.json`;
|
|
898
|
+
await this.upload(key, `${JSON.stringify(record)}
|
|
899
|
+
`);
|
|
900
|
+
return { ok: true };
|
|
901
|
+
}
|
|
902
|
+
return await this.addToBatch(record, this.batch);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
return this.fail(error);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Uploads any buffered entries now. Resolves ok when nothing is buffered.
|
|
909
|
+
* Entries from a failed upload stay buffered and are retried by the next flush.
|
|
910
|
+
*/
|
|
911
|
+
flush() {
|
|
912
|
+
const result = this.flushQueue.then(
|
|
913
|
+
() => this.uploadBuffered()
|
|
914
|
+
);
|
|
915
|
+
this.flushQueue = result.catch(() => void 0);
|
|
916
|
+
return result;
|
|
917
|
+
}
|
|
918
|
+
async addToBatch(record, batch) {
|
|
919
|
+
this.buffer.push(record);
|
|
920
|
+
const limit = batch.batchSize * BUFFER_LIMIT_MULTIPLIER;
|
|
921
|
+
if (this.buffer.length > limit) {
|
|
922
|
+
const dropped = this.buffer.length - limit;
|
|
923
|
+
this.buffer = this.buffer.slice(dropped);
|
|
924
|
+
console.error(
|
|
925
|
+
`${this.logPrefix} S3ErrorAdapter: dropped the ${dropped} oldest buffered entries because uploads to bucket "${this.bucket}" keep failing and the buffer is full (${limit} entries).`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
if (this.buffer.length >= batch.batchSize) {
|
|
929
|
+
return this.flush();
|
|
930
|
+
}
|
|
931
|
+
this.scheduleFlush(batch);
|
|
932
|
+
return { ok: true };
|
|
933
|
+
}
|
|
934
|
+
scheduleFlush(batch) {
|
|
935
|
+
if (batch.maxWaitMs === void 0 || this.timer !== null) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const timer = setTimeout(() => {
|
|
939
|
+
this.timer = null;
|
|
940
|
+
void this.flush();
|
|
941
|
+
}, batch.maxWaitMs);
|
|
942
|
+
timer.unref?.();
|
|
943
|
+
this.timer = timer;
|
|
944
|
+
}
|
|
945
|
+
async uploadBuffered() {
|
|
946
|
+
if (this.timer !== null) {
|
|
947
|
+
clearTimeout(this.timer);
|
|
948
|
+
this.timer = null;
|
|
949
|
+
}
|
|
950
|
+
const pending = this.buffer.slice();
|
|
951
|
+
if (pending.length === 0) {
|
|
952
|
+
return { ok: true };
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${crypto.randomUUID()}.ndjson`;
|
|
956
|
+
const body = pending.map((record) => JSON.stringify(record)).join("\n");
|
|
957
|
+
await this.upload(key, `${body}
|
|
958
|
+
`);
|
|
959
|
+
const uploaded = new Set(pending);
|
|
960
|
+
this.buffer = this.buffer.filter(
|
|
961
|
+
(record) => !uploaded.has(record)
|
|
962
|
+
);
|
|
963
|
+
return { ok: true };
|
|
964
|
+
} catch (error) {
|
|
965
|
+
if (this.batch !== void 0) {
|
|
966
|
+
this.scheduleFlush(this.batch);
|
|
967
|
+
}
|
|
968
|
+
return this.fail(error, pending.length);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
upload(key, body) {
|
|
972
|
+
return this.client.putObject({
|
|
973
|
+
Bucket: this.bucket,
|
|
974
|
+
Key: key,
|
|
975
|
+
Body: body,
|
|
976
|
+
ContentType: this.contentType
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
fail(error, bufferedCount) {
|
|
980
|
+
console.error(`${this.logPrefix} S3ErrorAdapter:`, error);
|
|
981
|
+
const retry = bufferedCount === void 0 ? "" : ` (${bufferedCount} buffered ${bufferedCount === 1 ? "entry was" : "entries were"} kept for the next flush)`;
|
|
982
|
+
return {
|
|
983
|
+
ok: false,
|
|
984
|
+
error: `S3ErrorAdapter failed to upload to bucket "${this.bucket}": ${describeError(error)}${retry}`
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// src/adapters/firebase-error-adapter.ts
|
|
990
|
+
var DEFAULT_WRITE_TIMEOUT_MS = 1e4;
|
|
991
|
+
function resolveTimeout(options, adapter) {
|
|
992
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WRITE_TIMEOUT_MS;
|
|
993
|
+
if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
994
|
+
throw new RangeError(
|
|
995
|
+
`${adapter}: timeoutMs must be a positive number or Infinity, received ${String(options.timeoutMs)}.`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
return timeoutMs;
|
|
999
|
+
}
|
|
1000
|
+
function waitForWrite(write, timeoutMs) {
|
|
1001
|
+
if (timeoutMs === Infinity) {
|
|
1002
|
+
return Promise.resolve(write);
|
|
1003
|
+
}
|
|
1004
|
+
return new Promise(
|
|
1005
|
+
(resolve, reject) => {
|
|
1006
|
+
const timer = setTimeout(() => {
|
|
1007
|
+
reject(
|
|
1008
|
+
new Error(
|
|
1009
|
+
`the write was not saved within ${timeoutMs} ms (the connection may be down; the SDK keeps the write queued and may still save it later)`
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
}, timeoutMs);
|
|
1013
|
+
write.then(
|
|
1014
|
+
(value) => {
|
|
1015
|
+
clearTimeout(timer);
|
|
1016
|
+
resolve(value);
|
|
1017
|
+
},
|
|
1018
|
+
(reason) => {
|
|
1019
|
+
clearTimeout(timer);
|
|
1020
|
+
reject(reason);
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
function buildRecord(entry) {
|
|
1027
|
+
const serialized = serializeEntry(entry);
|
|
1028
|
+
const record = {
|
|
1029
|
+
id: crypto.randomUUID(),
|
|
1030
|
+
code: serialized.code,
|
|
1031
|
+
message: serialized.message,
|
|
1032
|
+
timestamp: serialized.timestamp
|
|
1033
|
+
};
|
|
1034
|
+
if (serialized.source !== void 0) {
|
|
1035
|
+
record.source = serialized.source;
|
|
1036
|
+
}
|
|
1037
|
+
if (serialized.cause !== void 0) {
|
|
1038
|
+
record.cause = JSON.stringify(serialized.cause);
|
|
1039
|
+
}
|
|
1040
|
+
return record;
|
|
1041
|
+
}
|
|
1042
|
+
var FirestoreErrorAdapter = class {
|
|
1043
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1044
|
+
constructor(collection, options = {}) {
|
|
1045
|
+
this.collection = collection;
|
|
1046
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
1047
|
+
this.timeoutMs = resolveTimeout(options, "FirestoreErrorAdapter");
|
|
1048
|
+
}
|
|
1049
|
+
async log(entry) {
|
|
1050
|
+
try {
|
|
1051
|
+
const record = buildRecord(entry);
|
|
1052
|
+
await waitForWrite(
|
|
1053
|
+
this.collection.doc(record.id).set(record),
|
|
1054
|
+
this.timeoutMs
|
|
1055
|
+
);
|
|
1056
|
+
return { ok: true };
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
console.error(`${this.prefix} FirestoreErrorAdapter:`, error);
|
|
1059
|
+
return {
|
|
1060
|
+
ok: false,
|
|
1061
|
+
error: `FirestoreErrorAdapter failed to write the document: ${describeError(error)}`
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
var RealtimeDatabaseErrorAdapter = class {
|
|
1067
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1068
|
+
constructor(reference, options = {}) {
|
|
1069
|
+
this.reference = reference;
|
|
1070
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
1071
|
+
this.timeoutMs = resolveTimeout(options, "RealtimeDatabaseErrorAdapter");
|
|
1072
|
+
}
|
|
1073
|
+
async log(entry) {
|
|
1074
|
+
try {
|
|
1075
|
+
await waitForWrite(
|
|
1076
|
+
this.reference.push(buildRecord(entry)),
|
|
1077
|
+
this.timeoutMs
|
|
1078
|
+
);
|
|
1079
|
+
return { ok: true };
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
console.error(`${this.prefix} RealtimeDatabaseErrorAdapter:`, error);
|
|
1082
|
+
return {
|
|
1083
|
+
ok: false,
|
|
1084
|
+
error: `RealtimeDatabaseErrorAdapter failed to write the entry: ${describeError(error)}`
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// src/adapters/supabase-error-adapter.ts
|
|
1091
|
+
var IDENTIFIER_PATTERN3 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1092
|
+
function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME2, options = {}) {
|
|
1093
|
+
const insertRoles = options.insertRoles ?? [];
|
|
1094
|
+
for (const role of insertRoles) {
|
|
1095
|
+
if (!IDENTIFIER_PATTERN3.test(role)) {
|
|
1096
|
+
throw new RangeError(
|
|
1097
|
+
`buildSupabaseSetupSql: invalid role name "${role}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const statements = [
|
|
1102
|
+
...buildErrorLogTableDdl("postgres", tableName),
|
|
1103
|
+
`ALTER TABLE "${tableName}" ENABLE ROW LEVEL SECURITY`
|
|
1104
|
+
];
|
|
1105
|
+
if (insertRoles.length > 0) {
|
|
1106
|
+
const policy = `${tableName}_insert_only`;
|
|
1107
|
+
statements.push(
|
|
1108
|
+
`DROP POLICY IF EXISTS "${policy}" ON "${tableName}"`,
|
|
1109
|
+
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (true)`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
return statements;
|
|
1113
|
+
}
|
|
1114
|
+
function describeSupabaseError(result) {
|
|
1115
|
+
const error = result.error ?? { message: "" };
|
|
1116
|
+
const parts = [];
|
|
1117
|
+
if (error.message) {
|
|
1118
|
+
parts.push(error.message);
|
|
1119
|
+
} else if (result.status) {
|
|
1120
|
+
parts.push(
|
|
1121
|
+
`HTTP ${result.status}${result.statusText ? ` ${result.statusText}` : ""} with no error message (a 404 usually means the table does not exist or is not exposed by the API)`
|
|
1122
|
+
);
|
|
1123
|
+
} else {
|
|
1124
|
+
parts.push("unknown error with no message");
|
|
1125
|
+
}
|
|
1126
|
+
if (error.code) {
|
|
1127
|
+
parts.push(`code ${error.code}`);
|
|
1128
|
+
}
|
|
1129
|
+
const detailsLine = (error.details ?? "").split("\n")[0].trim();
|
|
1130
|
+
if (detailsLine && detailsLine !== error.message) {
|
|
1131
|
+
parts.push(`details: ${detailsLine}`);
|
|
1132
|
+
}
|
|
1133
|
+
if (error.hint) {
|
|
1134
|
+
parts.push(`hint: ${error.hint}`);
|
|
1135
|
+
}
|
|
1136
|
+
return parts.join("; ");
|
|
1137
|
+
}
|
|
1138
|
+
var SupabaseErrorAdapter = class {
|
|
1139
|
+
/** @throws {RangeError} if `options.tableName` is empty. */
|
|
1140
|
+
constructor(client, options = {}) {
|
|
1141
|
+
this.client = client;
|
|
1142
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME2;
|
|
1143
|
+
if (this.tableName.trim() === "") {
|
|
1144
|
+
throw new RangeError(
|
|
1145
|
+
"SupabaseErrorAdapter: tableName must not be empty."
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
this.prefix = options.prefix ?? "[error-log-adapter]";
|
|
1149
|
+
}
|
|
1150
|
+
async log(entry) {
|
|
1151
|
+
try {
|
|
1152
|
+
const serialized = serializeEntry(entry);
|
|
1153
|
+
const row = {
|
|
1154
|
+
id: crypto.randomUUID(),
|
|
1155
|
+
code: serialized.code,
|
|
1156
|
+
message: serialized.message,
|
|
1157
|
+
cause: serialized.cause === void 0 ? null : serialized.cause,
|
|
1158
|
+
source: serialized.source ?? null,
|
|
1159
|
+
occurred_at: serialized.timestamp
|
|
1160
|
+
};
|
|
1161
|
+
const result = await this.client.from(this.tableName).insert(row);
|
|
1162
|
+
if (result.error) {
|
|
1163
|
+
console.error(`${this.prefix} SupabaseErrorAdapter:`, result);
|
|
1164
|
+
return this.fail(describeSupabaseError(result));
|
|
1165
|
+
}
|
|
1166
|
+
return { ok: true };
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
console.error(`${this.prefix} SupabaseErrorAdapter:`, error);
|
|
1169
|
+
return this.fail(describeError(error));
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
fail(reason) {
|
|
1173
|
+
return {
|
|
1174
|
+
ok: false,
|
|
1175
|
+
error: `SupabaseErrorAdapter failed to insert into table "${this.tableName}": ${reason}`
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
631
1179
|
export {
|
|
632
1180
|
CompositeErrorAdapter,
|
|
633
1181
|
ConsoleErrorAdapter,
|
|
1182
|
+
FirestoreErrorAdapter,
|
|
634
1183
|
FlatFileErrorAdapter,
|
|
635
1184
|
IndexedDbErrorAdapter,
|
|
636
1185
|
JsonFileErrorAdapter,
|
|
637
1186
|
MongoDbErrorAdapter,
|
|
1187
|
+
MySqlErrorAdapter,
|
|
1188
|
+
PostgresErrorAdapter,
|
|
1189
|
+
RealtimeDatabaseErrorAdapter,
|
|
1190
|
+
RedisErrorAdapter,
|
|
1191
|
+
S3ErrorAdapter,
|
|
638
1192
|
SqliteErrorAdapter,
|
|
1193
|
+
SupabaseErrorAdapter,
|
|
639
1194
|
WebStorageErrorAdapter,
|
|
640
1195
|
WebhookErrorAdapter,
|
|
641
1196
|
buildCreateErrorLogTableSql,
|
|
1197
|
+
buildErrorLogTableDdl,
|
|
1198
|
+
buildSupabaseSetupSql,
|
|
642
1199
|
createErrorLogStore,
|
|
643
1200
|
formatErrorLogLine,
|
|
644
1201
|
serializeCause,
|