@pipedream/grain 0.0.1 → 1.0.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.
Files changed (27) hide show
  1. package/actions/get-recording/get-recording.mjs +92 -0
  2. package/actions/get-transcript/get-transcript.mjs +45 -0
  3. package/actions/list-recordings/list-recordings.mjs +89 -0
  4. package/common/constants.mjs +18 -0
  5. package/grain.app.mjs +159 -5
  6. package/package.json +4 -1
  7. package/sources/common/base.mjs +66 -0
  8. package/sources/common/highlight.mjs +38 -0
  9. package/sources/common/recording.mjs +66 -0
  10. package/sources/new-highlight-instant/new-highlight-instant.mjs +22 -0
  11. package/sources/new-highlight-instant/test-event.mjs +21 -0
  12. package/sources/new-recording-instant/new-recording-instant.mjs +22 -0
  13. package/sources/new-recording-instant/test-event.mjs +27 -0
  14. package/sources/new-story-instant/new-story-instant.mjs +28 -0
  15. package/sources/new-story-instant/test-event.mjs +17 -0
  16. package/sources/removed-highlight-instant/removed-highlight-instant.mjs +22 -0
  17. package/sources/removed-highlight-instant/test-event.mjs +8 -0
  18. package/sources/removed-recording-instant/removed-recording-instant.mjs +22 -0
  19. package/sources/removed-recording-instant/test-event.mjs +7 -0
  20. package/sources/removed-story-instant/removed-story-instant.mjs +22 -0
  21. package/sources/removed-story-instant/test-event.mjs +7 -0
  22. package/sources/updated-highlight-instant/test-event.mjs +21 -0
  23. package/sources/updated-highlight-instant/updated-highlight-instant.mjs +22 -0
  24. package/sources/updated-recording-instant/test-event.mjs +27 -0
  25. package/sources/updated-recording-instant/updated-recording-instant.mjs +22 -0
  26. package/sources/updated-story-instant/test-event.mjs +17 -0
  27. package/sources/updated-story-instant/updated-story-instant.mjs +28 -0
@@ -0,0 +1,92 @@
1
+ import grain from "../../grain.app.mjs";
2
+
3
+ export default {
4
+ key: "grain-get-recording",
5
+ name: "Get Recording",
6
+ description: "Fetches a specific recording by its ID from Grain, returning its metadata (title, times, URL, tags, teams, meeting type)."
7
+ + " Enable the optional include props to add highlights, participants, AI action items, AI summary, calendar event, HubSpot data, or screenshares to the response."
8
+ + " Use **List Recordings** to find recording IDs, and **Get Transcript** to fetch the full transcript."
9
+ + " [See the documentation](https://developers.grain.com/#get-recording)",
10
+ version: "1.0.0",
11
+ annotations: {
12
+ destructiveHint: false,
13
+ openWorldHint: true,
14
+ readOnlyHint: true,
15
+ },
16
+ type: "action",
17
+ props: {
18
+ grain,
19
+ recordingId: {
20
+ propDefinition: [
21
+ grain,
22
+ "recordingId",
23
+ ],
24
+ },
25
+ highlights: {
26
+ propDefinition: [
27
+ grain,
28
+ "highlights",
29
+ ],
30
+ },
31
+ participants: {
32
+ propDefinition: [
33
+ grain,
34
+ "participants",
35
+ ],
36
+ },
37
+ aiActionItems: {
38
+ propDefinition: [
39
+ grain,
40
+ "aiActionItems",
41
+ ],
42
+ },
43
+ aiSummary: {
44
+ propDefinition: [
45
+ grain,
46
+ "aiSummary",
47
+ ],
48
+ },
49
+ calendarEvent: {
50
+ propDefinition: [
51
+ grain,
52
+ "calendarEvent",
53
+ ],
54
+ },
55
+ hubspot: {
56
+ propDefinition: [
57
+ grain,
58
+ "hubspot",
59
+ ],
60
+ },
61
+ screenshares: {
62
+ type: "boolean",
63
+ label: "Include Screenshares",
64
+ description: "Include the recording's screenshare ranges in the response",
65
+ optional: true,
66
+ },
67
+ },
68
+ async run({ $ }) {
69
+ const include = {
70
+ highlights: this.highlights,
71
+ participants: this.participants,
72
+ ai_action_items: this.aiActionItems,
73
+ ai_summary: this.aiSummary,
74
+ calendar_event: this.calendarEvent,
75
+ hubspot: this.hubspot,
76
+ screenshares: this.screenshares,
77
+ };
78
+
79
+ const response = await this.grain.fetchRecording({
80
+ $,
81
+ recordingId: this.recordingId,
82
+ data: {
83
+ include: Object.fromEntries(Object.entries(include).filter(([
84
+ , value,
85
+ ]) => value)),
86
+ },
87
+ });
88
+
89
+ $.export("$summary", `Successfully fetched recording with ID ${this.recordingId}`);
90
+ return response;
91
+ },
92
+ };
@@ -0,0 +1,45 @@
1
+ import { TRANSCRIPT_FORMAT_OPTIONS } from "../../common/constants.mjs";
2
+ import grain from "../../grain.app.mjs";
3
+
4
+ export default {
5
+ key: "grain-get-transcript",
6
+ name: "Get Transcript",
7
+ description: "Fetches the full transcript of a Grain recording."
8
+ + " The `json` format returns structured segments with speaker, participant ID, start/end times in milliseconds, and text;"
9
+ + " `txt`, `vtt`, and `srt` return plain text or subtitle formats."
10
+ + " Use **List Recordings** to find recording IDs; use **Get Recording** for the recording's metadata instead of its transcript."
11
+ + " [See the documentation](https://developers.grain.com/#get-recording-transcript-json)",
12
+ version: "0.0.1",
13
+ annotations: {
14
+ destructiveHint: false,
15
+ openWorldHint: true,
16
+ readOnlyHint: true,
17
+ },
18
+ type: "action",
19
+ props: {
20
+ grain,
21
+ recordingId: {
22
+ propDefinition: [
23
+ grain,
24
+ "recordingId",
25
+ ],
26
+ },
27
+ format: {
28
+ type: "string",
29
+ label: "Format",
30
+ description: "Format for the transcript",
31
+ options: TRANSCRIPT_FORMAT_OPTIONS,
32
+ default: "json",
33
+ },
34
+ },
35
+ async run({ $ }) {
36
+ const response = await this.grain.fetchTranscript({
37
+ $,
38
+ recordingId: this.recordingId,
39
+ format: this.format,
40
+ });
41
+
42
+ $.export("$summary", `Successfully fetched transcript for recording ${this.recordingId}`);
43
+ return response;
44
+ },
45
+ };
@@ -0,0 +1,89 @@
1
+ import grain from "../../grain.app.mjs";
2
+
3
+ export default {
4
+ key: "grain-list-recordings",
5
+ name: "List Recordings",
6
+ description: "Lists Grain recordings, optionally filtered by start datetime range (ISO8601), title search, or participant scope."
7
+ + " Automatically paginates and returns up to Max Results recordings."
8
+ + " Use this to find recording IDs for **Get Recording** and **Get Transcript**."
9
+ + " [See the documentation](https://developers.grain.com/#list-recordings)",
10
+ version: "0.0.1",
11
+ annotations: {
12
+ destructiveHint: false,
13
+ openWorldHint: true,
14
+ readOnlyHint: true,
15
+ },
16
+ type: "action",
17
+ props: {
18
+ grain,
19
+ beforeDatetime: {
20
+ type: "string",
21
+ label: "Before Datetime",
22
+ description: "Only return recordings that started before this ISO8601 datetime. E.g. `2025-01-01T00:00:00Z`",
23
+ optional: true,
24
+ },
25
+ afterDatetime: {
26
+ type: "string",
27
+ label: "After Datetime",
28
+ description: "Only return recordings that started after this ISO8601 datetime. E.g. `2025-01-01T00:00:00Z`",
29
+ optional: true,
30
+ },
31
+ titleSearch: {
32
+ type: "string",
33
+ label: "Title Search",
34
+ description: "Only return recordings whose title matches this search string",
35
+ optional: true,
36
+ },
37
+ participantScope: {
38
+ type: "string",
39
+ label: "Participant Scope",
40
+ description: "Only return recordings whose participants are all internal, or that include external participants",
41
+ options: [
42
+ "internal",
43
+ "external",
44
+ ],
45
+ optional: true,
46
+ },
47
+ maxResults: {
48
+ type: "integer",
49
+ label: "Max Results",
50
+ description: "Maximum number of recordings to return. Must be a positive integer.",
51
+ optional: true,
52
+ default: 100,
53
+ min: 1,
54
+ },
55
+ },
56
+ async run({ $ }) {
57
+ const filter = {
58
+ before_datetime: this.beforeDatetime,
59
+ after_datetime: this.afterDatetime,
60
+ title_search: this.titleSearch,
61
+ participant_scope: this.participantScope,
62
+ };
63
+
64
+ const recordings = [];
65
+ let cursor;
66
+ do {
67
+ const {
68
+ recordings: page, cursor: nextCursor,
69
+ } = await this.grain.listRecordings({
70
+ $,
71
+ data: {
72
+ cursor,
73
+ filter,
74
+ },
75
+ });
76
+ recordings.push(...page);
77
+ cursor = nextCursor;
78
+ } while (cursor && recordings.length < this.maxResults);
79
+
80
+ if (recordings.length > this.maxResults) {
81
+ recordings.length = this.maxResults;
82
+ }
83
+
84
+ $.export("$summary", `Successfully fetched ${recordings.length} recording${recordings.length === 1
85
+ ? ""
86
+ : "s"}`);
87
+ return recordings;
88
+ },
89
+ };
@@ -0,0 +1,18 @@
1
+ export const TRANSCRIPT_FORMAT_OPTIONS = [
2
+ {
3
+ label: "JSON",
4
+ value: "json",
5
+ },
6
+ {
7
+ label: "Text",
8
+ value: "txt",
9
+ },
10
+ {
11
+ label: "VTT",
12
+ value: "vtt",
13
+ },
14
+ {
15
+ label: "SRT",
16
+ value: "srt",
17
+ },
18
+ ];
package/grain.app.mjs CHANGED
@@ -1,11 +1,165 @@
1
+ import { axios } from "@pipedream/platform";
2
+
1
3
  export default {
2
4
  type: "app",
3
5
  app: "grain",
4
- propDefinitions: {},
6
+ propDefinitions: {
7
+ recordingId: {
8
+ type: "string",
9
+ label: "Recording ID",
10
+ description: "The ID of the recording to fetch. Use **List Recordings** to find recording IDs.",
11
+ async options({ prevContext }) {
12
+ const {
13
+ recordings, cursor,
14
+ } = await this.listRecordings({
15
+ data: {
16
+ cursor: prevContext?.nextPage,
17
+ },
18
+ });
19
+ return {
20
+ options: recordings.map(({
21
+ id: value, title: label,
22
+ }) => ({
23
+ value,
24
+ label,
25
+ })),
26
+ context: {
27
+ nextPage: cursor,
28
+ },
29
+ };
30
+ },
31
+ },
32
+ highlights: {
33
+ type: "boolean",
34
+ label: "Include Highlights",
35
+ description: "Whether to include the recording's highlights",
36
+ optional: true,
37
+ },
38
+ participants: {
39
+ type: "boolean",
40
+ label: "Include Participants",
41
+ description: "Whether to include the recording's participants",
42
+ optional: true,
43
+ },
44
+ calendarEvent: {
45
+ type: "boolean",
46
+ label: "Include Calendar Event",
47
+ description: "Whether to include the recording's calendar event data",
48
+ optional: true,
49
+ },
50
+ hubspot: {
51
+ type: "boolean",
52
+ label: "Include HubSpot Data",
53
+ description: "Whether to include associated HubSpot data",
54
+ optional: true,
55
+ },
56
+ aiActionItems: {
57
+ type: "boolean",
58
+ label: "Include AI Action Items",
59
+ description: "Whether to include the recording's AI action items",
60
+ optional: true,
61
+ },
62
+ aiSummary: {
63
+ type: "boolean",
64
+ label: "Include AI Summary",
65
+ description: "Whether to include the recording's AI summary",
66
+ optional: true,
67
+ },
68
+ transcript: {
69
+ type: "boolean",
70
+ label: "Include Transcript",
71
+ description: "Whether to include the highlight's transcript",
72
+ optional: true,
73
+ },
74
+ speakers: {
75
+ type: "boolean",
76
+ label: "Include Speakers",
77
+ description: "Whether to include the highlight's speakers",
78
+ optional: true,
79
+ },
80
+ },
5
81
  methods: {
6
- // this.$auth contains connected account data
7
- authKeys() {
8
- console.log(Object.keys(this.$auth));
82
+ _baseUrl() {
83
+ return "https://api.grain.com/_/public-api/v2";
84
+ },
85
+ _headers() {
86
+ return {
87
+ "Authorization": `Bearer ${this.$auth.oauth_access_token}`,
88
+ "Public-Api-Version": "2025-10-31",
89
+ };
90
+ },
91
+ _makeRequest({
92
+ $ = this, path, ...opts
93
+ }) {
94
+ return axios($, {
95
+ url: this._baseUrl() + path,
96
+ headers: this._headers(),
97
+ ...opts,
98
+ });
99
+ },
100
+ /**
101
+ * Fetch a page of recordings matching the supplied filters.
102
+ * @param {object} [opts={}] Request context and data containing filter, include, and cursor.
103
+ * @returns {Promise<object>} Recordings and the cursor for the next page.
104
+ */
105
+ listRecordings(opts = {}) {
106
+ return this._makeRequest({
107
+ method: "POST",
108
+ path: "/recordings",
109
+ ...opts,
110
+ });
111
+ },
112
+ /**
113
+ * Fetch recording metadata and optional related data.
114
+ * @param {object} opts Request context, recordingId, and data containing include options.
115
+ * @returns {Promise<object>} The recording.
116
+ */
117
+ fetchRecording({
118
+ recordingId, ...opts
119
+ }) {
120
+ return this._makeRequest({
121
+ method: "POST",
122
+ path: `/recordings/${recordingId}`,
123
+ ...opts,
124
+ });
125
+ },
126
+ /**
127
+ * Fetch a recording's transcript in the requested format.
128
+ * @param {object} opts Request context, recordingId, and format (json, txt, vtt, or srt).
129
+ * @returns {Promise<object[]|string>} Transcript segments for JSON, or transcript text.
130
+ */
131
+ fetchTranscript({
132
+ recordingId, format, ...opts
133
+ }) {
134
+ return this._makeRequest({
135
+ path: `/recordings/${recordingId}/transcript${format === "json"
136
+ ? ""
137
+ : `.${format}`}`,
138
+ ...opts,
139
+ });
140
+ },
141
+ /**
142
+ * Register a webhook for a Grain event type.
143
+ * @param {object} [opts={}] Request options with hook_url, hook_type, and include in data.
144
+ * @returns {Promise<object>} The registered hook, including its ID.
145
+ */
146
+ createWebhook(opts = {}) {
147
+ return this._makeRequest({
148
+ method: "POST",
149
+ path: "/hooks/create",
150
+ ...opts,
151
+ });
152
+ },
153
+ /**
154
+ * Remove a webhook registration.
155
+ * @param {string} hookId The ID returned when the hook was created.
156
+ * @returns {Promise<object>} The API's success response.
157
+ */
158
+ deleteWebhook(hookId) {
159
+ return this._makeRequest({
160
+ method: "DELETE",
161
+ path: `/hooks/${hookId}`,
162
+ });
9
163
  },
10
164
  },
11
- };
165
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipedream/grain",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "Pipedream Grain Components",
5
5
  "main": "grain.app.mjs",
6
6
  "keywords": [
@@ -11,5 +11,8 @@
11
11
  "author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
12
12
  "publishConfig": {
13
13
  "access": "public"
14
+ },
15
+ "dependencies": {
16
+ "@pipedream/platform": "^3.1.1"
14
17
  }
15
18
  }
@@ -0,0 +1,66 @@
1
+ import grain from "../../grain.app.mjs";
2
+
3
+ export default {
4
+ props: {
5
+ grain,
6
+ http: {
7
+ type: "$.interface.http",
8
+ customResponse: true,
9
+ },
10
+ db: "$.service.db",
11
+ },
12
+ methods: {
13
+ _getHookId() {
14
+ return this.db.get("hookId");
15
+ },
16
+ _setHookId(hookId) {
17
+ this.db.set("hookId", hookId);
18
+ },
19
+ getInclude() {
20
+ return undefined;
21
+ },
22
+ getTimestamp() {
23
+ return Date.now();
24
+ },
25
+ },
26
+ hooks: {
27
+ async activate() {
28
+ const response = await this.grain.createWebhook({
29
+ data: {
30
+ hook_url: this.http.endpoint,
31
+ hook_type: this.getHookType(),
32
+ include: this.getInclude(),
33
+ },
34
+ });
35
+ this._setHookId(response.id);
36
+ },
37
+ async deactivate() {
38
+ const webhookId = this._getHookId();
39
+ if (webhookId) {
40
+ await this.grain.deleteWebhook(webhookId);
41
+ }
42
+ },
43
+ },
44
+ async run({ body }) {
45
+ this.http.respond({
46
+ status: 200,
47
+ });
48
+
49
+ if (!body?.data?.id || body.type !== this.getHookType()) return;
50
+
51
+ const ts = this.getTimestamp(body);
52
+ // Grain doesn't document a delivery ID. Added/deleted events dedupe on the
53
+ // resource ID alone (there's only ever one). Updated events concatenate the
54
+ // resource ID with the payload-derived timestamp so retries of the same
55
+ // update share an ID while a later, distinct update gets a new one.
56
+ const id = body.type.endsWith("_updated")
57
+ ? `${body.data.id}:${ts}`
58
+ : body.data.id;
59
+
60
+ this.$emit(body, {
61
+ id,
62
+ summary: this.getSummary(body),
63
+ ts,
64
+ });
65
+ },
66
+ };
@@ -0,0 +1,38 @@
1
+ import common from "./base.mjs";
2
+
3
+ export default {
4
+ ...common,
5
+ props: {
6
+ ...common.props,
7
+ transcript: {
8
+ propDefinition: [
9
+ common.props.grain,
10
+ "transcript",
11
+ ],
12
+ },
13
+ speakers: {
14
+ propDefinition: [
15
+ common.props.grain,
16
+ "speakers",
17
+ ],
18
+ },
19
+ },
20
+ methods: {
21
+ ...common.methods,
22
+ getInclude() {
23
+ const include = {
24
+ transcript: this.transcript,
25
+ speakers: this.speakers,
26
+ };
27
+ return Object.fromEntries(Object.entries(include).filter(([
28
+ , value,
29
+ ]) => value));
30
+ },
31
+ getTimestamp({ data }) {
32
+ const ts = Date.parse(data.created_datetime);
33
+ return Number.isNaN(ts)
34
+ ? Date.now()
35
+ : ts;
36
+ },
37
+ },
38
+ };
@@ -0,0 +1,66 @@
1
+ import common from "./base.mjs";
2
+
3
+ export default {
4
+ ...common,
5
+ props: {
6
+ ...common.props,
7
+ highlights: {
8
+ propDefinition: [
9
+ common.props.grain,
10
+ "highlights",
11
+ ],
12
+ },
13
+ participants: {
14
+ propDefinition: [
15
+ common.props.grain,
16
+ "participants",
17
+ ],
18
+ },
19
+ calendarEvent: {
20
+ propDefinition: [
21
+ common.props.grain,
22
+ "calendarEvent",
23
+ ],
24
+ },
25
+ hubspot: {
26
+ propDefinition: [
27
+ common.props.grain,
28
+ "hubspot",
29
+ ],
30
+ },
31
+ aiActionItems: {
32
+ propDefinition: [
33
+ common.props.grain,
34
+ "aiActionItems",
35
+ ],
36
+ },
37
+ aiSummary: {
38
+ propDefinition: [
39
+ common.props.grain,
40
+ "aiSummary",
41
+ ],
42
+ },
43
+ },
44
+ methods: {
45
+ ...common.methods,
46
+ getInclude() {
47
+ const include = {
48
+ highlights: this.highlights,
49
+ participants: this.participants,
50
+ calendar_event: this.calendarEvent,
51
+ hubspot: this.hubspot,
52
+ ai_action_items: this.aiActionItems,
53
+ ai_summary: this.aiSummary,
54
+ };
55
+ return Object.fromEntries(Object.entries(include).filter(([
56
+ , value,
57
+ ]) => value));
58
+ },
59
+ getTimestamp({ data }) {
60
+ const ts = Date.parse(data.end_datetime);
61
+ return Number.isNaN(ts)
62
+ ? Date.now()
63
+ : ts;
64
+ },
65
+ },
66
+ };
@@ -0,0 +1,22 @@
1
+ import common from "../common/highlight.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-new-highlight-instant",
7
+ name: "New Highlight (Instant)",
8
+ description: "Emit new event when a highlight is added. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "highlight_added";
16
+ },
17
+ getSummary({ data }) {
18
+ return `New highlight added: ${data.id}`;
19
+ },
20
+ },
21
+ sampleEmit,
22
+ };
@@ -0,0 +1,21 @@
1
+ export default {
2
+ "type": "highlight_added",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "a14e5af9-d28e-43e9-902b-bc07419082eb",
6
+ "recording_id": "b5185ccb-9a08-458c-9be1-db17a03fb14c",
7
+ "text": "testing 123 #test",
8
+ "transcript": "expected, that there was a mews in a lane which runs down by one wall of the garden. I lent the ostlers a hand in rubbing down their horses, and received in exchange twopence, a glass of half-and-half, two fills of shag tobacco, and as much information as I could desire about Miss Adler, to say nothing of half a dozen other people in",
9
+ "speakers": [
10
+ "Andy Arbol"
11
+ ],
12
+ "timestamp": 3080,
13
+ "duration": 15000,
14
+ "created_datetime": "2021-07-29T23:16:34Z",
15
+ "url": "https://grain.com/highlight/a14e5af9-d28e-43e9-902b-bc07419082eb",
16
+ "thumbnail_url": "https://media.grain.com/clips/v1/a14e5af9-d28e-43e9-902b-bc07419082eb/57zB8z52l7BKPoOvkS9KNyUi7LDSsNEh.jpeg",
17
+ "tags": [
18
+ "test"
19
+ ]
20
+ }
21
+ };
@@ -0,0 +1,22 @@
1
+ import common from "../common/recording.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-new-recording-instant",
7
+ name: "New Recording (Instant)",
8
+ description: "Emit new event when a recording is added. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "recording_added";
16
+ },
17
+ getSummary({ data }) {
18
+ return `New recording added: ${data.id}`;
19
+ },
20
+ },
21
+ sampleEmit,
22
+ };
@@ -0,0 +1,27 @@
1
+ export default {
2
+ "type": "recording_added",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "pppp6666-qq77-rr88-ss99-tttt00000000",
6
+ "title": "All Hands",
7
+ "start_datetime": "2025-01-01T09:30:00Z",
8
+ "end_datetime": "2025-01-01T10:00:00Z",
9
+ "duration_ms": 1800000,
10
+ "media_type": "video",
11
+ "source": "zoom",
12
+ "url": "https://grain.com/share/recording/pppp6666-qq77-rr88-ss99-tttt00000000",
13
+ "thumbnail_url": "https://media.grain.com/public_thumbnails/recordings/pppp6666",
14
+ "tags": [],
15
+ "teams": [
16
+ {
17
+ "id": "aaaa1111-bb22-cc33-dd44-eeee55555555",
18
+ "name": "My Team",
19
+ },
20
+ ],
21
+ "meeting_type": {
22
+ "id": "ffff6666-gg77-hh88-ii99-jjjj00000000",
23
+ "name": "Project & Team Coordination",
24
+ "scope": "internal",
25
+ },
26
+ },
27
+ };
@@ -0,0 +1,28 @@
1
+ import common from "../common/base.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-new-story-instant",
7
+ name: "New Story (Instant)",
8
+ description: "Emit new event when a story is added. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "story_added";
16
+ },
17
+ getTimestamp({ data }) {
18
+ const ts = Date.parse(data.created_datetime);
19
+ return Number.isNaN(ts)
20
+ ? Date.now()
21
+ : ts;
22
+ },
23
+ getSummary({ data }) {
24
+ return `New story added: ${data.id}`;
25
+ },
26
+ },
27
+ sampleEmit,
28
+ };
@@ -0,0 +1,17 @@
1
+ export default {
2
+ "type": "story_added",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "1aff0fe4-6575-4d5f-a462-aaf09f5f17a6",
6
+ "title": "My customer story",
7
+ "description": "A customer journey with ACME Corp",
8
+ "url": "https://grain.com/app/stories/89bd4a02-25f5-42c0-bd40-aa4c94be13ce",
9
+ "public_url": "https://grain.com/share/story/89bd4a02-25f5-42c0-bd40-aa4c94be13ce/2hAEpxLsIN8hDQ48aQ1Yi1MIirv1qCPSJNhxXEoj",
10
+ "banner_image_url": "https://media.grain.com/public/story_thumbnails/07.png",
11
+ "created_datetime": "2021-07-29T23:16:34Z",
12
+ "last_edited_datetime": "2021-08-29T23:16:34Z",
13
+ "tags": [
14
+ "customer"
15
+ ]
16
+ }
17
+ };
@@ -0,0 +1,22 @@
1
+ import common from "../common/base.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-removed-highlight-instant",
7
+ name: "New Highlight Removed (Instant)",
8
+ description: "Emit new event when a highlight is removed. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "highlight_deleted";
16
+ },
17
+ getSummary({ data }) {
18
+ return `Highlight removed: ${data.id}`;
19
+ },
20
+ },
21
+ sampleEmit,
22
+ };
@@ -0,0 +1,8 @@
1
+ export default {
2
+ "type": "highlight_deleted",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "a14e5af9-d28e-43e9-902b-bc07419082eb",
6
+ "recording_id": "b5185ccb-9a08-458c-9be1-db17a03fb14c"
7
+ }
8
+ };
@@ -0,0 +1,22 @@
1
+ import common from "../common/base.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-removed-recording-instant",
7
+ name: "New Recording Removed (Instant)",
8
+ description: "Emit new event when a recording is removed. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "recording_deleted";
16
+ },
17
+ getSummary({ data }) {
18
+ return `Recording removed: ${data.id}`;
19
+ },
20
+ },
21
+ sampleEmit,
22
+ };
@@ -0,0 +1,7 @@
1
+ export default {
2
+ "type": "recording_deleted",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "b5185ccb-9a08-458c-9be1-db17a03fb14c"
6
+ }
7
+ };
@@ -0,0 +1,22 @@
1
+ import common from "../common/base.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-removed-story-instant",
7
+ name: "New Story Removed (Instant)",
8
+ description: "Emit new event when a story is removed. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "story_deleted";
16
+ },
17
+ getSummary({ data }) {
18
+ return `New story removed: ${data.id}`;
19
+ },
20
+ },
21
+ sampleEmit,
22
+ };
@@ -0,0 +1,7 @@
1
+ export default {
2
+ "type": "story_deleted",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "1aff0fe4-6575-4d5f-a462-aaf09f5f17a6"
6
+ }
7
+ };
@@ -0,0 +1,21 @@
1
+ export default {
2
+ "type": "highlight_updated",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "a14e5af9-d28e-43e9-902b-bc07419082eb",
6
+ "recording_id": "b5185ccb-9a08-458c-9be1-db17a03fb14c",
7
+ "text": "testing 123 #test",
8
+ "transcript": "expected, that there was a mews in a lane which runs down by one wall of the garden. I lent the ostlers a hand in rubbing down their horses, and received in exchange twopence, a glass of half-and-half, two fills of shag tobacco, and as much information as I could desire about Miss Adler, to say nothing of half a dozen other people in",
9
+ "speakers": [
10
+ "Andy Arbol"
11
+ ],
12
+ "timestamp": 3080,
13
+ "duration": 15000,
14
+ "created_datetime": "2021-07-29T23:16:34Z",
15
+ "url": "https://grain.com/highlight/a14e5af9-d28e-43e9-902b-bc07419082eb",
16
+ "thumbnail_url": "https://media.grain.com/clips/v1/a14e5af9-d28e-43e9-902b-bc07419082eb/57zB8z52l7BKPoOvkS9KNyUi7LDSsNEh.jpeg",
17
+ "tags": [
18
+ "test"
19
+ ]
20
+ }
21
+ };
@@ -0,0 +1,22 @@
1
+ import common from "../common/highlight.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-updated-highlight-instant",
7
+ name: "New Highlight Updated (Instant)",
8
+ description: "Emit new event when a highlight is updated. Each webhook delivery emits an event, including retries. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ // Grain does not document a delivery ID; deduping by resource ID would discard later updates.
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "highlight_updated";
16
+ },
17
+ getSummary({ data }) {
18
+ return `New highlight updated: ${data.id}`;
19
+ },
20
+ },
21
+ sampleEmit,
22
+ };
@@ -0,0 +1,27 @@
1
+ export default {
2
+ "type": "recording_updated",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "pppp6666-qq77-rr88-ss99-tttt00000000",
6
+ "title": "All Hands — Updated",
7
+ "start_datetime": "2025-01-01T09:30:00Z",
8
+ "end_datetime": "2025-01-01T10:00:00Z",
9
+ "duration_ms": 1800000,
10
+ "media_type": "video",
11
+ "source": "zoom",
12
+ "url": "https://grain.com/share/recording/pppp6666-qq77-rr88-ss99-tttt00000000",
13
+ "thumbnail_url": "https://media.grain.com/public_thumbnails/recordings/pppp6666",
14
+ "tags": [],
15
+ "teams": [
16
+ {
17
+ "id": "aaaa1111-bb22-cc33-dd44-eeee55555555",
18
+ "name": "My Team",
19
+ },
20
+ ],
21
+ "meeting_type": {
22
+ "id": "ffff6666-gg77-hh88-ii99-jjjj00000000",
23
+ "name": "Project & Team Coordination",
24
+ "scope": "internal",
25
+ },
26
+ },
27
+ };
@@ -0,0 +1,22 @@
1
+ import common from "../common/recording.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-updated-recording-instant",
7
+ name: "New Recording Updated (Instant)",
8
+ description: "Emit new event when a recording is updated. Each webhook delivery emits an event, including retries. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ // Grain does not document a delivery ID; deduping by resource ID would discard later updates.
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "recording_updated";
16
+ },
17
+ getSummary({ data }) {
18
+ return `New recording updated: ${data.id}`;
19
+ },
20
+ },
21
+ sampleEmit,
22
+ };
@@ -0,0 +1,17 @@
1
+ export default {
2
+ "type": "story_updated",
3
+ "user_id": "aea95745-99e9-4609-8623-c9efa2926b82",
4
+ "data": {
5
+ "id": "1aff0fe4-6575-4d5f-a462-aaf09f5f17a6",
6
+ "title": "My customer story",
7
+ "description": "A customer journey with ACME Corp",
8
+ "url": "https://grain.com/app/stories/89bd4a02-25f5-42c0-bd40-aa4c94be13ce",
9
+ "public_url": "https://grain.com/share/story/89bd4a02-25f5-42c0-bd40-aa4c94be13ce/2hAEpxLsIN8hDQ48aQ1Yi1MIirv1qCPSJNhxXEoj",
10
+ "banner_image_url": "https://media.grain.com/public/story_thumbnails/07.png",
11
+ "created_datetime": "2021-07-29T23:16:34Z",
12
+ "last_edited_datetime": "2021-08-29T23:16:34Z",
13
+ "tags": [
14
+ "customer"
15
+ ]
16
+ }
17
+ };
@@ -0,0 +1,28 @@
1
+ import common from "../common/base.mjs";
2
+ import sampleEmit from "./test-event.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "grain-updated-story-instant",
7
+ name: "New Story Updated (Instant)",
8
+ description: "Emit new event when a story is updated. Each webhook delivery emits an event, including retries. [See the documentation](https://developers.grain.com/#create-hook)",
9
+ version: "1.0.0",
10
+ type: "source",
11
+ // Grain does not document a delivery ID; deduping by resource ID would discard later updates.
12
+ methods: {
13
+ ...common.methods,
14
+ getHookType() {
15
+ return "story_updated";
16
+ },
17
+ getTimestamp({ data }) {
18
+ const ts = Date.parse(data.last_edited_datetime);
19
+ return Number.isNaN(ts)
20
+ ? Date.now()
21
+ : ts;
22
+ },
23
+ getSummary({ data }) {
24
+ return `New story updated: ${data.id}`;
25
+ },
26
+ },
27
+ sampleEmit,
28
+ };