@geekmidas/storage 10.0.0-alpha.122 → 10.0.0-alpha.124

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.
@@ -0,0 +1,98 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/events.ts
3
+ /** The event types a bucket's subscribers can name. */
4
+ const OBJECT_EVENT_TYPES = ["object.created", "object.deleted"];
5
+ /** A notification body that is not one the storage sends. */
6
+ var ObjectNotificationMalformed = class extends Error {
7
+ reason;
8
+ constructor(reason) {
9
+ super(`An object notification could not be read: ${reason}. A bucket's notifications are a JSON body with a 'Records' array, as S3 and MinIO send them — check what is publishing to this subscriber.`);
10
+ this.reason = reason;
11
+ this.name = "ObjectNotificationMalformed";
12
+ }
13
+ };
14
+ /**
15
+ * The object events in a notification body — S3's or MinIO's, as an object
16
+ * or as the JSON string SNS carries it in.
17
+ *
18
+ * A test event — S3 sends one when a notification is configured — is no
19
+ * event, so it reads as none. So does any record that is neither a write nor
20
+ * a delete: a restore, a replication, a lifecycle transition.
21
+ *
22
+ * @throws {ObjectNotificationMalformed} when the body is not a notification
23
+ */
24
+ function toObjectEvents(body) {
25
+ const notification = typeof body === "string" ? parse(body) : body;
26
+ if (!isRecord(notification)) throw new ObjectNotificationMalformed("it is not a JSON object");
27
+ if (isTestEvent(notification)) return [];
28
+ const { Records: records } = notification;
29
+ if (!Array.isArray(records)) throw new ObjectNotificationMalformed("it has no Records array");
30
+ const messages = [];
31
+ for (const record of records) {
32
+ if (!isRecord(record)) throw new ObjectNotificationMalformed("a record is not an object");
33
+ const type = eventType(record.eventName);
34
+ if (!type) continue;
35
+ const s3 = isRecord(record.s3) ? record.s3 : void 0;
36
+ const bucket = isRecord(s3?.bucket) ? s3.bucket.name : void 0;
37
+ const object = isRecord(s3?.object) ? s3.object : void 0;
38
+ if (typeof bucket !== "string" || typeof object?.key !== "string") throw new ObjectNotificationMalformed("a record names no bucket or no object key");
39
+ const payload = {
40
+ bucket,
41
+ key: decodeKey(object.key)
42
+ };
43
+ if (typeof object.size === "number") payload.size = object.size;
44
+ const etag = object.eTag ?? object.etag;
45
+ if (typeof etag === "string" && etag) payload.etag = etag.replace(/^"|"$/g, "");
46
+ if (typeof object.versionId === "string" && object.versionId) payload.versionId = object.versionId;
47
+ messages.push({
48
+ type,
49
+ payload
50
+ });
51
+ }
52
+ return messages;
53
+ }
54
+ /** Whether the event's key is under the filter's prefix and ends in its suffix. */
55
+ function matchesObjectFilter(event, filter = {}) {
56
+ if (filter.prefix && !event.key.startsWith(filter.prefix)) return false;
57
+ if (filter.suffix && !event.key.endsWith(filter.suffix)) return false;
58
+ return true;
59
+ }
60
+ /** `ObjectCreated:Put` or `s3:ObjectCreated:Put` → `object.created`. */
61
+ function eventType(name) {
62
+ if (typeof name !== "string") return void 0;
63
+ const bare = name.startsWith("s3:") ? name.slice(3) : name;
64
+ if (bare.startsWith("ObjectCreated:")) return "object.created";
65
+ if (bare.startsWith("ObjectRemoved:")) return "object.deleted";
66
+ }
67
+ /**
68
+ * A key as the storage encodes it in a notification: form-encoded, so a
69
+ * space is `+` and a literal `+` is `%2B`.
70
+ */
71
+ function decodeKey(key) {
72
+ const spaced = key.replace(/\+/g, " ");
73
+ try {
74
+ return decodeURIComponent(spaced);
75
+ } catch {
76
+ return spaced;
77
+ }
78
+ }
79
+ function isTestEvent(notification) {
80
+ return notification.Event === "s3:TestEvent" || notification.EventName === "s3:TestEvent";
81
+ }
82
+ function parse(body) {
83
+ try {
84
+ return JSON.parse(body);
85
+ } catch {
86
+ throw new ObjectNotificationMalformed("it is not JSON");
87
+ }
88
+ }
89
+ function isRecord(value) {
90
+ return typeof value === "object" && value !== null && !Array.isArray(value);
91
+ }
92
+ //#endregion
93
+ exports.OBJECT_EVENT_TYPES = OBJECT_EVENT_TYPES;
94
+ exports.ObjectNotificationMalformed = ObjectNotificationMalformed;
95
+ exports.matchesObjectFilter = matchesObjectFilter;
96
+ exports.toObjectEvents = toObjectEvents;
97
+
98
+ //# sourceMappingURL=events.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.cjs","names":[],"sources":["../src/events.ts"],"sourcesContent":["/**\n * Object events — what a bucket says happened to its objects, in words that\n * name no cloud.\n *\n * S3 and MinIO notify in the same shape: a body with `Records`, each naming\n * the event (`ObjectCreated:Put`, or `s3:ObjectCreated:Put` on MinIO) and the\n * object, its key URL-encoded the way a form is (`a+b.png` for `a b.png`).\n * This reads either into `{ type: 'object.created', payload }`, the message\n * shape every subscriber already handles.\n */\n\n/** What happened to an object. */\nexport type ObjectEventType = 'object.created' | 'object.deleted';\n\n/** The event types a bucket's subscribers can name. */\nexport const OBJECT_EVENT_TYPES: readonly ObjectEventType[] = [\n\t'object.created',\n\t'object.deleted',\n];\n\n/** One object, as an event names it. */\nexport interface ObjectEvent {\n\t/** The bucket's name, as the storage knows it. */\n\tbucket: string;\n\t/** The object's key, decoded. */\n\tkey: string;\n\t/** Its size in bytes. Absent on a delete. */\n\tsize?: number;\n\t/** Its entity tag, quotes removed. Absent on a delete. */\n\tetag?: string;\n\t/** The version written or removed, on a versioned bucket. */\n\tversionId?: string;\n}\n\n/** An object event as a message: what a subscriber is handed. */\nexport interface ObjectMessage<\n\tTType extends ObjectEventType = ObjectEventType,\n> {\n\ttype: TType;\n\tpayload: ObjectEvent;\n}\n\n/** Which keys a subscriber wants: those under `prefix` and ending in `suffix`. */\nexport interface ObjectFilter {\n\tprefix?: string;\n\tsuffix?: string;\n}\n\n/** A notification body that is not one the storage sends. */\nexport class ObjectNotificationMalformed extends Error {\n\tconstructor(readonly reason: string) {\n\t\tsuper(\n\t\t\t`An object notification could not be read: ${reason}. A bucket's ` +\n\t\t\t\t`notifications are a JSON body with a 'Records' array, as S3 and ` +\n\t\t\t\t`MinIO send them — check what is publishing to this subscriber.`,\n\t\t);\n\t\tthis.name = 'ObjectNotificationMalformed';\n\t}\n}\n\n/**\n * The object events in a notification body — S3's or MinIO's, as an object\n * or as the JSON string SNS carries it in.\n *\n * A test event — S3 sends one when a notification is configured — is no\n * event, so it reads as none. So does any record that is neither a write nor\n * a delete: a restore, a replication, a lifecycle transition.\n *\n * @throws {ObjectNotificationMalformed} when the body is not a notification\n */\nexport function toObjectEvents(body: unknown): ObjectMessage[] {\n\tconst notification = typeof body === 'string' ? parse(body) : body;\n\tif (!isRecord(notification)) {\n\t\tthrow new ObjectNotificationMalformed('it is not a JSON object');\n\t}\n\tif (isTestEvent(notification)) return [];\n\n\tconst { Records: records } = notification;\n\tif (!Array.isArray(records)) {\n\t\tthrow new ObjectNotificationMalformed('it has no Records array');\n\t}\n\n\tconst messages: ObjectMessage[] = [];\n\tfor (const record of records) {\n\t\tif (!isRecord(record)) {\n\t\t\tthrow new ObjectNotificationMalformed('a record is not an object');\n\t\t}\n\t\tconst type = eventType(record.eventName);\n\t\tif (!type) continue;\n\n\t\tconst s3 = isRecord(record.s3) ? record.s3 : undefined;\n\t\tconst bucket = isRecord(s3?.bucket) ? s3.bucket.name : undefined;\n\t\tconst object = isRecord(s3?.object) ? s3.object : undefined;\n\t\tif (typeof bucket !== 'string' || typeof object?.key !== 'string') {\n\t\t\tthrow new ObjectNotificationMalformed(\n\t\t\t\t'a record names no bucket or no object key',\n\t\t\t);\n\t\t}\n\n\t\tconst payload: ObjectEvent = { bucket, key: decodeKey(object.key) };\n\t\tif (typeof object.size === 'number') payload.size = object.size;\n\t\tconst etag = object.eTag ?? object.etag;\n\t\tif (typeof etag === 'string' && etag) {\n\t\t\tpayload.etag = etag.replace(/^\"|\"$/g, '');\n\t\t}\n\t\tif (typeof object.versionId === 'string' && object.versionId) {\n\t\t\tpayload.versionId = object.versionId;\n\t\t}\n\t\tmessages.push({ type, payload });\n\t}\n\treturn messages;\n}\n\n/** Whether the event's key is under the filter's prefix and ends in its suffix. */\nexport function matchesObjectFilter(\n\tevent: Pick<ObjectEvent, 'key'>,\n\tfilter: ObjectFilter = {},\n): boolean {\n\tif (filter.prefix && !event.key.startsWith(filter.prefix)) return false;\n\tif (filter.suffix && !event.key.endsWith(filter.suffix)) return false;\n\treturn true;\n}\n\n/** `ObjectCreated:Put` or `s3:ObjectCreated:Put` → `object.created`. */\nfunction eventType(name: unknown): ObjectEventType | undefined {\n\tif (typeof name !== 'string') return undefined;\n\tconst bare = name.startsWith('s3:') ? name.slice(3) : name;\n\tif (bare.startsWith('ObjectCreated:')) return 'object.created';\n\tif (bare.startsWith('ObjectRemoved:')) return 'object.deleted';\n\treturn undefined;\n}\n\n/**\n * A key as the storage encodes it in a notification: form-encoded, so a\n * space is `+` and a literal `+` is `%2B`.\n */\nfunction decodeKey(key: string): string {\n\tconst spaced = key.replace(/\\+/g, ' ');\n\ttry {\n\t\treturn decodeURIComponent(spaced);\n\t} catch {\n\t\t// A lone `%` the storage did not encode: the key as written.\n\t\treturn spaced;\n\t}\n}\n\nfunction isTestEvent(notification: Record<string, unknown>): boolean {\n\treturn (\n\t\tnotification.Event === 's3:TestEvent' ||\n\t\tnotification.EventName === 's3:TestEvent'\n\t);\n}\n\nfunction parse(body: string): unknown {\n\ttry {\n\t\treturn JSON.parse(body);\n\t} catch {\n\t\tthrow new ObjectNotificationMalformed('it is not JSON');\n\t}\n}\n\nfunction isRecord(value: unknown): value is Record<string, any> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"],"mappings":";;;AAeA,MAAa,qBAAiD,CAC7D,kBACA,gBACD;;AA+BA,IAAa,8BAAb,cAAiD,MAAM;CACjC;CAArB,YAAY,QAAyB;EACpC,MACC,6CAA6C,OAAO,4IAGrD;EALoB,KAAA,SAAA;EAMpB,KAAK,OAAO;CACb;AACD;;;;;;;;;;;AAYA,SAAgB,eAAe,MAAgC;CAC9D,MAAM,eAAe,OAAO,SAAS,WAAW,MAAM,IAAI,IAAI;CAC9D,IAAI,CAAC,SAAS,YAAY,GACzB,MAAM,IAAI,4BAA4B,yBAAyB;CAEhE,IAAI,YAAY,YAAY,GAAG,OAAO,CAAC;CAEvC,MAAM,EAAE,SAAS,YAAY;CAC7B,IAAI,CAAC,MAAM,QAAQ,OAAO,GACzB,MAAM,IAAI,4BAA4B,yBAAyB;CAGhE,MAAM,WAA4B,CAAC;CACnC,KAAK,MAAM,UAAU,SAAS;EAC7B,IAAI,CAAC,SAAS,MAAM,GACnB,MAAM,IAAI,4BAA4B,2BAA2B;EAElE,MAAM,OAAO,UAAU,OAAO,SAAS;EACvC,IAAI,CAAC,MAAM;EAEX,MAAM,KAAK,SAAS,OAAO,EAAE,IAAI,OAAO,KAAK,KAAA;EAC7C,MAAM,SAAS,SAAS,IAAI,MAAM,IAAI,GAAG,OAAO,OAAO,KAAA;EACvD,MAAM,SAAS,SAAS,IAAI,MAAM,IAAI,GAAG,SAAS,KAAA;EAClD,IAAI,OAAO,WAAW,YAAY,OAAO,QAAQ,QAAQ,UACxD,MAAM,IAAI,4BACT,2CACD;EAGD,MAAM,UAAuB;GAAE;GAAQ,KAAK,UAAU,OAAO,GAAG;EAAE;EAClE,IAAI,OAAO,OAAO,SAAS,UAAU,QAAQ,OAAO,OAAO;EAC3D,MAAM,OAAO,OAAO,QAAQ,OAAO;EACnC,IAAI,OAAO,SAAS,YAAY,MAC/B,QAAQ,OAAO,KAAK,QAAQ,UAAU,EAAE;EAEzC,IAAI,OAAO,OAAO,cAAc,YAAY,OAAO,WAClD,QAAQ,YAAY,OAAO;EAE5B,SAAS,KAAK;GAAE;GAAM;EAAQ,CAAC;CAChC;CACA,OAAO;AACR;;AAGA,SAAgB,oBACf,OACA,SAAuB,CAAC,GACd;CACV,IAAI,OAAO,UAAU,CAAC,MAAM,IAAI,WAAW,OAAO,MAAM,GAAG,OAAO;CAClE,IAAI,OAAO,UAAU,CAAC,MAAM,IAAI,SAAS,OAAO,MAAM,GAAG,OAAO;CAChE,OAAO;AACR;;AAGA,SAAS,UAAU,MAA4C;CAC9D,IAAI,OAAO,SAAS,UAAU,OAAO,KAAA;CACrC,MAAM,OAAO,KAAK,WAAW,KAAK,IAAI,KAAK,MAAM,CAAC,IAAI;CACtD,IAAI,KAAK,WAAW,gBAAgB,GAAG,OAAO;CAC9C,IAAI,KAAK,WAAW,gBAAgB,GAAG,OAAO;AAE/C;;;;;AAMA,SAAS,UAAU,KAAqB;CACvC,MAAM,SAAS,IAAI,QAAQ,OAAO,GAAG;CACrC,IAAI;EACH,OAAO,mBAAmB,MAAM;CACjC,QAAQ;EAEP,OAAO;CACR;AACD;AAEA,SAAS,YAAY,cAAgD;CACpE,OACC,aAAa,UAAU,kBACvB,aAAa,cAAc;AAE7B;AAEA,SAAS,MAAM,MAAuB;CACrC,IAAI;EACH,OAAO,KAAK,MAAM,IAAI;CACvB,QAAQ;EACP,MAAM,IAAI,4BAA4B,gBAAgB;CACvD;AACD;AAEA,SAAS,SAAS,OAA8C;CAC/D,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC3E"}
@@ -0,0 +1,58 @@
1
+ //#region src/events.d.ts
2
+ /**
3
+ * Object events — what a bucket says happened to its objects, in words that
4
+ * name no cloud.
5
+ *
6
+ * S3 and MinIO notify in the same shape: a body with `Records`, each naming
7
+ * the event (`ObjectCreated:Put`, or `s3:ObjectCreated:Put` on MinIO) and the
8
+ * object, its key URL-encoded the way a form is (`a+b.png` for `a b.png`).
9
+ * This reads either into `{ type: 'object.created', payload }`, the message
10
+ * shape every subscriber already handles.
11
+ */
12
+ /** What happened to an object. */
13
+ export type ObjectEventType = 'object.created' | 'object.deleted';
14
+ /** The event types a bucket's subscribers can name. */
15
+ export declare const OBJECT_EVENT_TYPES: readonly ObjectEventType[];
16
+ /** One object, as an event names it. */
17
+ export interface ObjectEvent {
18
+ /** The bucket's name, as the storage knows it. */
19
+ bucket: string;
20
+ /** The object's key, decoded. */
21
+ key: string;
22
+ /** Its size in bytes. Absent on a delete. */
23
+ size?: number;
24
+ /** Its entity tag, quotes removed. Absent on a delete. */
25
+ etag?: string;
26
+ /** The version written or removed, on a versioned bucket. */
27
+ versionId?: string;
28
+ }
29
+ /** An object event as a message: what a subscriber is handed. */
30
+ export interface ObjectMessage<TType extends ObjectEventType = ObjectEventType> {
31
+ type: TType;
32
+ payload: ObjectEvent;
33
+ }
34
+ /** Which keys a subscriber wants: those under `prefix` and ending in `suffix`. */
35
+ export interface ObjectFilter {
36
+ prefix?: string;
37
+ suffix?: string;
38
+ }
39
+ /** A notification body that is not one the storage sends. */
40
+ export declare class ObjectNotificationMalformed extends Error {
41
+ readonly reason: string;
42
+ constructor(reason: string);
43
+ }
44
+ /**
45
+ * The object events in a notification body — S3's or MinIO's, as an object
46
+ * or as the JSON string SNS carries it in.
47
+ *
48
+ * A test event — S3 sends one when a notification is configured — is no
49
+ * event, so it reads as none. So does any record that is neither a write nor
50
+ * a delete: a restore, a replication, a lifecycle transition.
51
+ *
52
+ * @throws {ObjectNotificationMalformed} when the body is not a notification
53
+ */
54
+ export declare function toObjectEvents(body: unknown): ObjectMessage[];
55
+ /** Whether the event's key is under the filter's prefix and ends in its suffix. */
56
+ export declare function matchesObjectFilter(event: Pick<ObjectEvent, 'key'>, filter?: ObjectFilter): boolean;
57
+ //#endregion
58
+ //# sourceMappingURL=events.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.cts","names":[],"sources":["../src/events.ts"],"mappings":";;;;;;;;;;;;YAYY;;qBAGC,6BAA6B;;iBAMzB;;EAEhB;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIgB,cAChB,cAAc,kBAAkB;EAEhC,MAAM;EACN,SAAS;;;iBAIO;EAChB;EACA;;;qBAIY,oCAAoC;WAC3B;EAArB,YAAqB;;;;;;;;;;;;wBAoBN,eAAe,gBAAgB;;wBA4C/B,oBACf,OAAO,KAAK,qBACZ,SAAQ"}
@@ -0,0 +1,58 @@
1
+ //#region src/events.d.ts
2
+ /**
3
+ * Object events — what a bucket says happened to its objects, in words that
4
+ * name no cloud.
5
+ *
6
+ * S3 and MinIO notify in the same shape: a body with `Records`, each naming
7
+ * the event (`ObjectCreated:Put`, or `s3:ObjectCreated:Put` on MinIO) and the
8
+ * object, its key URL-encoded the way a form is (`a+b.png` for `a b.png`).
9
+ * This reads either into `{ type: 'object.created', payload }`, the message
10
+ * shape every subscriber already handles.
11
+ */
12
+ /** What happened to an object. */
13
+ export type ObjectEventType = 'object.created' | 'object.deleted';
14
+ /** The event types a bucket's subscribers can name. */
15
+ export declare const OBJECT_EVENT_TYPES: readonly ObjectEventType[];
16
+ /** One object, as an event names it. */
17
+ export interface ObjectEvent {
18
+ /** The bucket's name, as the storage knows it. */
19
+ bucket: string;
20
+ /** The object's key, decoded. */
21
+ key: string;
22
+ /** Its size in bytes. Absent on a delete. */
23
+ size?: number;
24
+ /** Its entity tag, quotes removed. Absent on a delete. */
25
+ etag?: string;
26
+ /** The version written or removed, on a versioned bucket. */
27
+ versionId?: string;
28
+ }
29
+ /** An object event as a message: what a subscriber is handed. */
30
+ export interface ObjectMessage<TType extends ObjectEventType = ObjectEventType> {
31
+ type: TType;
32
+ payload: ObjectEvent;
33
+ }
34
+ /** Which keys a subscriber wants: those under `prefix` and ending in `suffix`. */
35
+ export interface ObjectFilter {
36
+ prefix?: string;
37
+ suffix?: string;
38
+ }
39
+ /** A notification body that is not one the storage sends. */
40
+ export declare class ObjectNotificationMalformed extends Error {
41
+ readonly reason: string;
42
+ constructor(reason: string);
43
+ }
44
+ /**
45
+ * The object events in a notification body — S3's or MinIO's, as an object
46
+ * or as the JSON string SNS carries it in.
47
+ *
48
+ * A test event — S3 sends one when a notification is configured — is no
49
+ * event, so it reads as none. So does any record that is neither a write nor
50
+ * a delete: a restore, a replication, a lifecycle transition.
51
+ *
52
+ * @throws {ObjectNotificationMalformed} when the body is not a notification
53
+ */
54
+ export declare function toObjectEvents(body: unknown): ObjectMessage[];
55
+ /** Whether the event's key is under the filter's prefix and ends in its suffix. */
56
+ export declare function matchesObjectFilter(event: Pick<ObjectEvent, 'key'>, filter?: ObjectFilter): boolean;
57
+ //#endregion
58
+ //# sourceMappingURL=events.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.mts","names":[],"sources":["../src/events.ts"],"mappings":";;;;;;;;;;;;YAYY;;qBAGC,6BAA6B;;iBAMzB;;EAEhB;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIgB,cAChB,cAAc,kBAAkB;EAEhC,MAAM;EACN,SAAS;;;iBAIO;EAChB;EACA;;;qBAIY,oCAAoC;WAC3B;EAArB,YAAqB;;;;;;;;;;;;wBAoBN,eAAe,gBAAgB;;wBA4C/B,oBACf,OAAO,KAAK,qBACZ,SAAQ"}
@@ -0,0 +1,94 @@
1
+ //#region src/events.ts
2
+ /** The event types a bucket's subscribers can name. */
3
+ const OBJECT_EVENT_TYPES = ["object.created", "object.deleted"];
4
+ /** A notification body that is not one the storage sends. */
5
+ var ObjectNotificationMalformed = class extends Error {
6
+ reason;
7
+ constructor(reason) {
8
+ super(`An object notification could not be read: ${reason}. A bucket's notifications are a JSON body with a 'Records' array, as S3 and MinIO send them — check what is publishing to this subscriber.`);
9
+ this.reason = reason;
10
+ this.name = "ObjectNotificationMalformed";
11
+ }
12
+ };
13
+ /**
14
+ * The object events in a notification body — S3's or MinIO's, as an object
15
+ * or as the JSON string SNS carries it in.
16
+ *
17
+ * A test event — S3 sends one when a notification is configured — is no
18
+ * event, so it reads as none. So does any record that is neither a write nor
19
+ * a delete: a restore, a replication, a lifecycle transition.
20
+ *
21
+ * @throws {ObjectNotificationMalformed} when the body is not a notification
22
+ */
23
+ function toObjectEvents(body) {
24
+ const notification = typeof body === "string" ? parse(body) : body;
25
+ if (!isRecord(notification)) throw new ObjectNotificationMalformed("it is not a JSON object");
26
+ if (isTestEvent(notification)) return [];
27
+ const { Records: records } = notification;
28
+ if (!Array.isArray(records)) throw new ObjectNotificationMalformed("it has no Records array");
29
+ const messages = [];
30
+ for (const record of records) {
31
+ if (!isRecord(record)) throw new ObjectNotificationMalformed("a record is not an object");
32
+ const type = eventType(record.eventName);
33
+ if (!type) continue;
34
+ const s3 = isRecord(record.s3) ? record.s3 : void 0;
35
+ const bucket = isRecord(s3?.bucket) ? s3.bucket.name : void 0;
36
+ const object = isRecord(s3?.object) ? s3.object : void 0;
37
+ if (typeof bucket !== "string" || typeof object?.key !== "string") throw new ObjectNotificationMalformed("a record names no bucket or no object key");
38
+ const payload = {
39
+ bucket,
40
+ key: decodeKey(object.key)
41
+ };
42
+ if (typeof object.size === "number") payload.size = object.size;
43
+ const etag = object.eTag ?? object.etag;
44
+ if (typeof etag === "string" && etag) payload.etag = etag.replace(/^"|"$/g, "");
45
+ if (typeof object.versionId === "string" && object.versionId) payload.versionId = object.versionId;
46
+ messages.push({
47
+ type,
48
+ payload
49
+ });
50
+ }
51
+ return messages;
52
+ }
53
+ /** Whether the event's key is under the filter's prefix and ends in its suffix. */
54
+ function matchesObjectFilter(event, filter = {}) {
55
+ if (filter.prefix && !event.key.startsWith(filter.prefix)) return false;
56
+ if (filter.suffix && !event.key.endsWith(filter.suffix)) return false;
57
+ return true;
58
+ }
59
+ /** `ObjectCreated:Put` or `s3:ObjectCreated:Put` → `object.created`. */
60
+ function eventType(name) {
61
+ if (typeof name !== "string") return void 0;
62
+ const bare = name.startsWith("s3:") ? name.slice(3) : name;
63
+ if (bare.startsWith("ObjectCreated:")) return "object.created";
64
+ if (bare.startsWith("ObjectRemoved:")) return "object.deleted";
65
+ }
66
+ /**
67
+ * A key as the storage encodes it in a notification: form-encoded, so a
68
+ * space is `+` and a literal `+` is `%2B`.
69
+ */
70
+ function decodeKey(key) {
71
+ const spaced = key.replace(/\+/g, " ");
72
+ try {
73
+ return decodeURIComponent(spaced);
74
+ } catch {
75
+ return spaced;
76
+ }
77
+ }
78
+ function isTestEvent(notification) {
79
+ return notification.Event === "s3:TestEvent" || notification.EventName === "s3:TestEvent";
80
+ }
81
+ function parse(body) {
82
+ try {
83
+ return JSON.parse(body);
84
+ } catch {
85
+ throw new ObjectNotificationMalformed("it is not JSON");
86
+ }
87
+ }
88
+ function isRecord(value) {
89
+ return typeof value === "object" && value !== null && !Array.isArray(value);
90
+ }
91
+ //#endregion
92
+ export { OBJECT_EVENT_TYPES, ObjectNotificationMalformed, matchesObjectFilter, toObjectEvents };
93
+
94
+ //# sourceMappingURL=events.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.mjs","names":[],"sources":["../src/events.ts"],"sourcesContent":["/**\n * Object events — what a bucket says happened to its objects, in words that\n * name no cloud.\n *\n * S3 and MinIO notify in the same shape: a body with `Records`, each naming\n * the event (`ObjectCreated:Put`, or `s3:ObjectCreated:Put` on MinIO) and the\n * object, its key URL-encoded the way a form is (`a+b.png` for `a b.png`).\n * This reads either into `{ type: 'object.created', payload }`, the message\n * shape every subscriber already handles.\n */\n\n/** What happened to an object. */\nexport type ObjectEventType = 'object.created' | 'object.deleted';\n\n/** The event types a bucket's subscribers can name. */\nexport const OBJECT_EVENT_TYPES: readonly ObjectEventType[] = [\n\t'object.created',\n\t'object.deleted',\n];\n\n/** One object, as an event names it. */\nexport interface ObjectEvent {\n\t/** The bucket's name, as the storage knows it. */\n\tbucket: string;\n\t/** The object's key, decoded. */\n\tkey: string;\n\t/** Its size in bytes. Absent on a delete. */\n\tsize?: number;\n\t/** Its entity tag, quotes removed. Absent on a delete. */\n\tetag?: string;\n\t/** The version written or removed, on a versioned bucket. */\n\tversionId?: string;\n}\n\n/** An object event as a message: what a subscriber is handed. */\nexport interface ObjectMessage<\n\tTType extends ObjectEventType = ObjectEventType,\n> {\n\ttype: TType;\n\tpayload: ObjectEvent;\n}\n\n/** Which keys a subscriber wants: those under `prefix` and ending in `suffix`. */\nexport interface ObjectFilter {\n\tprefix?: string;\n\tsuffix?: string;\n}\n\n/** A notification body that is not one the storage sends. */\nexport class ObjectNotificationMalformed extends Error {\n\tconstructor(readonly reason: string) {\n\t\tsuper(\n\t\t\t`An object notification could not be read: ${reason}. A bucket's ` +\n\t\t\t\t`notifications are a JSON body with a 'Records' array, as S3 and ` +\n\t\t\t\t`MinIO send them — check what is publishing to this subscriber.`,\n\t\t);\n\t\tthis.name = 'ObjectNotificationMalformed';\n\t}\n}\n\n/**\n * The object events in a notification body — S3's or MinIO's, as an object\n * or as the JSON string SNS carries it in.\n *\n * A test event — S3 sends one when a notification is configured — is no\n * event, so it reads as none. So does any record that is neither a write nor\n * a delete: a restore, a replication, a lifecycle transition.\n *\n * @throws {ObjectNotificationMalformed} when the body is not a notification\n */\nexport function toObjectEvents(body: unknown): ObjectMessage[] {\n\tconst notification = typeof body === 'string' ? parse(body) : body;\n\tif (!isRecord(notification)) {\n\t\tthrow new ObjectNotificationMalformed('it is not a JSON object');\n\t}\n\tif (isTestEvent(notification)) return [];\n\n\tconst { Records: records } = notification;\n\tif (!Array.isArray(records)) {\n\t\tthrow new ObjectNotificationMalformed('it has no Records array');\n\t}\n\n\tconst messages: ObjectMessage[] = [];\n\tfor (const record of records) {\n\t\tif (!isRecord(record)) {\n\t\t\tthrow new ObjectNotificationMalformed('a record is not an object');\n\t\t}\n\t\tconst type = eventType(record.eventName);\n\t\tif (!type) continue;\n\n\t\tconst s3 = isRecord(record.s3) ? record.s3 : undefined;\n\t\tconst bucket = isRecord(s3?.bucket) ? s3.bucket.name : undefined;\n\t\tconst object = isRecord(s3?.object) ? s3.object : undefined;\n\t\tif (typeof bucket !== 'string' || typeof object?.key !== 'string') {\n\t\t\tthrow new ObjectNotificationMalformed(\n\t\t\t\t'a record names no bucket or no object key',\n\t\t\t);\n\t\t}\n\n\t\tconst payload: ObjectEvent = { bucket, key: decodeKey(object.key) };\n\t\tif (typeof object.size === 'number') payload.size = object.size;\n\t\tconst etag = object.eTag ?? object.etag;\n\t\tif (typeof etag === 'string' && etag) {\n\t\t\tpayload.etag = etag.replace(/^\"|\"$/g, '');\n\t\t}\n\t\tif (typeof object.versionId === 'string' && object.versionId) {\n\t\t\tpayload.versionId = object.versionId;\n\t\t}\n\t\tmessages.push({ type, payload });\n\t}\n\treturn messages;\n}\n\n/** Whether the event's key is under the filter's prefix and ends in its suffix. */\nexport function matchesObjectFilter(\n\tevent: Pick<ObjectEvent, 'key'>,\n\tfilter: ObjectFilter = {},\n): boolean {\n\tif (filter.prefix && !event.key.startsWith(filter.prefix)) return false;\n\tif (filter.suffix && !event.key.endsWith(filter.suffix)) return false;\n\treturn true;\n}\n\n/** `ObjectCreated:Put` or `s3:ObjectCreated:Put` → `object.created`. */\nfunction eventType(name: unknown): ObjectEventType | undefined {\n\tif (typeof name !== 'string') return undefined;\n\tconst bare = name.startsWith('s3:') ? name.slice(3) : name;\n\tif (bare.startsWith('ObjectCreated:')) return 'object.created';\n\tif (bare.startsWith('ObjectRemoved:')) return 'object.deleted';\n\treturn undefined;\n}\n\n/**\n * A key as the storage encodes it in a notification: form-encoded, so a\n * space is `+` and a literal `+` is `%2B`.\n */\nfunction decodeKey(key: string): string {\n\tconst spaced = key.replace(/\\+/g, ' ');\n\ttry {\n\t\treturn decodeURIComponent(spaced);\n\t} catch {\n\t\t// A lone `%` the storage did not encode: the key as written.\n\t\treturn spaced;\n\t}\n}\n\nfunction isTestEvent(notification: Record<string, unknown>): boolean {\n\treturn (\n\t\tnotification.Event === 's3:TestEvent' ||\n\t\tnotification.EventName === 's3:TestEvent'\n\t);\n}\n\nfunction parse(body: string): unknown {\n\ttry {\n\t\treturn JSON.parse(body);\n\t} catch {\n\t\tthrow new ObjectNotificationMalformed('it is not JSON');\n\t}\n}\n\nfunction isRecord(value: unknown): value is Record<string, any> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"],"mappings":";;AAeA,MAAa,qBAAiD,CAC7D,kBACA,gBACD;;AA+BA,IAAa,8BAAb,cAAiD,MAAM;CACjC;CAArB,YAAY,QAAyB;EACpC,MACC,6CAA6C,OAAO,4IAGrD;EALoB,KAAA,SAAA;EAMpB,KAAK,OAAO;CACb;AACD;;;;;;;;;;;AAYA,SAAgB,eAAe,MAAgC;CAC9D,MAAM,eAAe,OAAO,SAAS,WAAW,MAAM,IAAI,IAAI;CAC9D,IAAI,CAAC,SAAS,YAAY,GACzB,MAAM,IAAI,4BAA4B,yBAAyB;CAEhE,IAAI,YAAY,YAAY,GAAG,OAAO,CAAC;CAEvC,MAAM,EAAE,SAAS,YAAY;CAC7B,IAAI,CAAC,MAAM,QAAQ,OAAO,GACzB,MAAM,IAAI,4BAA4B,yBAAyB;CAGhE,MAAM,WAA4B,CAAC;CACnC,KAAK,MAAM,UAAU,SAAS;EAC7B,IAAI,CAAC,SAAS,MAAM,GACnB,MAAM,IAAI,4BAA4B,2BAA2B;EAElE,MAAM,OAAO,UAAU,OAAO,SAAS;EACvC,IAAI,CAAC,MAAM;EAEX,MAAM,KAAK,SAAS,OAAO,EAAE,IAAI,OAAO,KAAK,KAAA;EAC7C,MAAM,SAAS,SAAS,IAAI,MAAM,IAAI,GAAG,OAAO,OAAO,KAAA;EACvD,MAAM,SAAS,SAAS,IAAI,MAAM,IAAI,GAAG,SAAS,KAAA;EAClD,IAAI,OAAO,WAAW,YAAY,OAAO,QAAQ,QAAQ,UACxD,MAAM,IAAI,4BACT,2CACD;EAGD,MAAM,UAAuB;GAAE;GAAQ,KAAK,UAAU,OAAO,GAAG;EAAE;EAClE,IAAI,OAAO,OAAO,SAAS,UAAU,QAAQ,OAAO,OAAO;EAC3D,MAAM,OAAO,OAAO,QAAQ,OAAO;EACnC,IAAI,OAAO,SAAS,YAAY,MAC/B,QAAQ,OAAO,KAAK,QAAQ,UAAU,EAAE;EAEzC,IAAI,OAAO,OAAO,cAAc,YAAY,OAAO,WAClD,QAAQ,YAAY,OAAO;EAE5B,SAAS,KAAK;GAAE;GAAM;EAAQ,CAAC;CAChC;CACA,OAAO;AACR;;AAGA,SAAgB,oBACf,OACA,SAAuB,CAAC,GACd;CACV,IAAI,OAAO,UAAU,CAAC,MAAM,IAAI,WAAW,OAAO,MAAM,GAAG,OAAO;CAClE,IAAI,OAAO,UAAU,CAAC,MAAM,IAAI,SAAS,OAAO,MAAM,GAAG,OAAO;CAChE,OAAO;AACR;;AAGA,SAAS,UAAU,MAA4C;CAC9D,IAAI,OAAO,SAAS,UAAU,OAAO,KAAA;CACrC,MAAM,OAAO,KAAK,WAAW,KAAK,IAAI,KAAK,MAAM,CAAC,IAAI;CACtD,IAAI,KAAK,WAAW,gBAAgB,GAAG,OAAO;CAC9C,IAAI,KAAK,WAAW,gBAAgB,GAAG,OAAO;AAE/C;;;;;AAMA,SAAS,UAAU,KAAqB;CACvC,MAAM,SAAS,IAAI,QAAQ,OAAO,GAAG;CACrC,IAAI;EACH,OAAO,mBAAmB,MAAM;CACjC,QAAQ;EAEP,OAAO;CACR;AACD;AAEA,SAAS,YAAY,cAAgD;CACpE,OACC,aAAa,UAAU,kBACvB,aAAa,cAAc;AAE7B;AAEA,SAAS,MAAM,MAAuB;CACrC,IAAI;EACH,OAAO,KAAK,MAAM,IAAI;CACvB,QAAQ;EACP,MAAM,IAAI,4BAA4B,gBAAgB;CACvD;AACD;AAEA,SAAS,SAAS,OAA8C;CAC/D,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC3E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/storage",
3
- "version": "10.0.0-alpha.122",
3
+ "version": "10.0.0-alpha.124",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,6 +22,16 @@
22
22
  "default": "./dist/aws.cjs"
23
23
  }
24
24
  },
25
+ "./events": {
26
+ "import": {
27
+ "types": "./dist/events.d.mts",
28
+ "default": "./dist/events.mjs"
29
+ },
30
+ "require": {
31
+ "types": "./dist/events.d.cts",
32
+ "default": "./dist/events.cjs"
33
+ }
34
+ },
25
35
  ".": {
26
36
  "import": {
27
37
  "types": "./dist/index.d.mts",
@@ -54,7 +64,7 @@
54
64
  "@aws-sdk/client-s3": ">=3.1136.0",
55
65
  "@aws-sdk/s3-presigned-post": ">=3.1136.0",
56
66
  "@aws-sdk/s3-request-presigner": ">=3.1136.0",
57
- "@geekmidas/cache": "^10.0.0-alpha.122"
67
+ "@geekmidas/cache": "^10.0.0-alpha.124"
58
68
  },
59
69
  "peerDependenciesMeta": {
60
70
  "@geekmidas/cache": {
@@ -75,7 +85,7 @@
75
85
  },
76
86
  "devDependencies": {
77
87
  "@opentelemetry/sdk-trace-node": "~2.11.0",
78
- "@geekmidas/cache": "^10.0.0-alpha.122"
88
+ "@geekmidas/cache": "^10.0.0-alpha.124"
79
89
  },
80
90
  "scripts": {
81
91
  "build": "tsdown --config ../../tsdown.config.ts --filter ."