@geekmidas/storage 10.0.0-alpha.123 → 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.
- package/dist/events.cjs +98 -0
- package/dist/events.cjs.map +1 -0
- package/dist/events.d.cts +58 -0
- package/dist/events.d.cts.map +1 -0
- package/dist/events.d.mts +58 -0
- package/dist/events.d.mts.map +1 -0
- package/dist/events.mjs +94 -0
- package/dist/events.mjs.map +1 -0
- package/package.json +13 -3
package/dist/events.cjs
ADDED
|
@@ -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"}
|
package/dist/events.mjs
ADDED
|
@@ -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.
|
|
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.
|
|
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.
|
|
88
|
+
"@geekmidas/cache": "^10.0.0-alpha.124"
|
|
79
89
|
},
|
|
80
90
|
"scripts": {
|
|
81
91
|
"build": "tsdown --config ../../tsdown.config.ts --filter ."
|