diff --git a/plugins/Sanity/v1/configValidation.json b/plugins/Sanity/v1/configValidation.json new file mode 100644 index 00000000..98d4f124 --- /dev/null +++ b/plugins/Sanity/v1/configValidation.json @@ -0,0 +1,13 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { + "name": "listProjects" + }, + "required": true, + "success": "Connected to the Sanity management API.", + "error": "Could not list projects. Check your Admin API token is valid and has access to your projects." + } + ] +} diff --git a/plugins/Sanity/v1/custom_types.json b/plugins/Sanity/v1/custom_types.json new file mode 100644 index 00000000..01d59910 --- /dev/null +++ b/plugins/Sanity/v1/custom_types.json @@ -0,0 +1,9 @@ +[ + { + "name": "Sanity Project", + "sourceType": "Sanity Project", + "icon": "layer-group", + "singular": "Sanity Project", + "plural": "Sanity Projects" + } +] diff --git a/plugins/Sanity/v1/dataStreams/changeHistory.json b/plugins/Sanity/v1/dataStreams/changeHistory.json new file mode 100644 index 00000000..6f977203 --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/changeHistory.json @@ -0,0 +1,72 @@ +{ + "name": "changeHistory", + "displayName": "Change History", + "description": "Transaction history for a Sanity dataset — every content change with timestamp, author, and affected document count", + "tags": ["Documents"], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Sanity Dataset" + } + }, + "config": { + "baseUrl": "https://{{object.projectId}}.api.sanity.io/{{dataSource.apiVersion}}/", + "httpMethod": "get", + "paging": { + "mode": "none" + }, + "endpointPath": "data/history/{{object.datasetId}}/transactions", + "getArgs": [ + { "key": "excludeContent", "value": "true" }, + { "key": "reverse", "value": "true" }, + { "key": "limit", "value": "100" }, + { "key": "fromTime", "value": "{{timeframe.start}}" }, + { "key": "toTime", "value": "{{timeframe.end}}" } + ], + "headers": [ + { + "key": "Authorization", + "value": "Bearer {{ ((dataSource.projects || []).find(p => p.key === object.projectId) || {}).value }}" + } + ], + "postRequestScript": "changeHistory.js" + }, + + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "transactionId", + "displayName": "Transaction ID", + "shape": "string", + "role": "id" + }, + { + "name": "author", + "displayName": "Author", + "shape": "string", + "role": "label" + }, + { + "name": "documentCount", + "displayName": "Documents Changed", + "shape": "number", + "role": "value" + } + ], + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days", + "lastMonth", + "lastQuarter", + "lastYear" + ] +} diff --git a/plugins/Sanity/v1/dataStreams/customQuery.json b/plugins/Sanity/v1/dataStreams/customQuery.json new file mode 100644 index 00000000..53840e2c --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/customQuery.json @@ -0,0 +1,65 @@ +{ + "name": "customQuery", + "displayName": "Custom GROQ Query", + "description": "Run an arbitrary GROQ query against a Sanity dataset", + "tags": ["Documents"], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Sanity Dataset" + } + }, + "providesPluginDiagnostics": true, + "config": { + "baseUrl": "https://{{object.projectId}}.api.sanity.io/{{dataSource.apiVersion}}/", + "httpMethod": "get", + "paging": { + "mode": "none" + }, + "expandInnerObjects": true, + "endpointPath": "data/query/{{object.datasetId}}", + "pathToData": "result", + "getArgs": [ + { + "key": "query", + "value": "{{query}}" + }, + { + "key": "perspective", + "value": "{{perspective}}" + } + ], + "headers": [ + { + "key": "Authorization", + "value": "Bearer {{ ((dataSource.projects || []).find(p => p.key === object.projectId) || {}).value }}" + } + ] + }, + "ui": [ + { + "type": "code", + "name": "query", + "language": "groq", + "label": "GROQ query", + "defaultValue": "*[0...20]{_id, _type, _updatedAt}", + "validation": { "required": true }, + "help": "See [GROQ query cheat sheet](https://www.sanity.io/docs/content-lake/query-cheat-sheet) for patterns" + }, + { + "type": "switch", + "name": "perspective", + "label": "Perspective", + "defaultValue": "published", + "options": [ + { "value": "published", "label": "Published" }, + { "value": "drafts", "label": "Drafts" }, + { "value": "raw", "label": "Raw" } + ] + } + ], + "metadata": [{ "pattern": ".*" }], + "timeframes": false, + "manualConfigApply": true +} diff --git a/plugins/Sanity/v1/dataStreams/datasetStats.json b/plugins/Sanity/v1/dataStreams/datasetStats.json new file mode 100644 index 00000000..d752ae4b --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/datasetStats.json @@ -0,0 +1,45 @@ +{ + "name": "datasetStats", + "displayName": "Dataset Stats", + "description": "Get stats on how close to usage limits this dataset is", + "tags": ["Analytics"], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Sanity Dataset" + } + }, + "providesPluginDiagnostics": true, + "config": { + "baseUrl": "https://{{object.projectId}}.api.sanity.io/v1/", + "httpMethod": "get", + "paging": { + "mode": "none" + }, + "endpointPath": "data/stats/{{object.datasetId}}", + "getArgs": [], + "headers": [ + { + "key": "Authorization", + "value": "Bearer {{ ((dataSource.projects || []).find(p => p.key === object.projectId) || {}).value }}" + } + ], + "expandInnerObjects": true + }, + "ui": [ + { + "type": "text", + "name": "hostname", + "label": "Hostname", + "placeholder": "api.example.com" + } + ], + "metadata": [ + { + "pattern": ".*" + } + ], + "timeframes": false, + "manualConfigApply": true +} diff --git a/plugins/Sanity/v1/dataStreams/datasets.json b/plugins/Sanity/v1/dataStreams/datasets.json new file mode 100644 index 00000000..3a26b6a6 --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/datasets.json @@ -0,0 +1,80 @@ +{ + "name": "datasets", + "displayName": "Datasets", + "description": "Lists the datasets in a Sanity project, with their access-control mode.", + "tags": ["Datasets"], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Sanity Project" + } + }, + "config": { + "httpMethod": "get", + "paging": { + "mode": "none" + }, + "expandInnerObjects": true, + "endpointPath": "projects/{{object.rawId}}/datasets", + "getArgs": [], + "postRequestScript": "datasets.js", + "headers": [ + { + "key": "Authorization", + "value": "Bearer {{ ((dataSource.projects || []).find(p => p.key === object.rawId) || {}).value }}" + } + ] + }, + "metadata": [ + { + "name": "projectId", + "displayName": "Project ID", + "shape": "string", + "visible": false + }, + { + "name": "datasetId", + "shape": "string", + "displayName": "Dataset", + "role": "label" + }, + { + "name": "aclMode", + "shape": "string", + "displayName": "Access mode" + }, + { + "name": "createdAt", + "shape": "date", + "displayName": "Created At" + }, + { + "name": "createdByUserId", + "shape": "string", + "displayName": "Created By User Id" + }, + { + "name": "projectName", + "displayName": "Project Name", + "shape": "string" + }, + { + "name": "uid", + "displayName": "uid", + "shape": "string", + "computed": true, + "valueExpression": "{{ $['projectId'] + '-' + $['datasetId'] }}", + "visible": false + }, + { + "name": "displayName", + "displayName": "Display Name", + "shape": "string", + "computed": true, + "valueExpression": "{{ $['projectName'] + ' - ' + $['datasetId'] }}", + "visible": false + } + ], + "timeframes": false +} diff --git a/plugins/Sanity/v1/dataStreams/documentTypeCounts.json b/plugins/Sanity/v1/dataStreams/documentTypeCounts.json new file mode 100644 index 00000000..73877882 --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/documentTypeCounts.json @@ -0,0 +1,51 @@ +{ + "name": "documentTypeCounts", + "displayName": "Document Type Counts", + "description": "Count of published documents per type in a Sanity dataset", + "tags": ["Documents"], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Sanity Dataset" + } + }, + "config": { + "baseUrl": "https://{{object.projectId}}.api.sanity.io/{{dataSource.apiVersion}}/", + "httpMethod": "get", + "paging": { + "mode": "none" + }, + "endpointPath": "data/query/{{object.datasetId}}", + "getArgs": [ + { + "key": "query", + "value": "*[!(_id in path(\"drafts.**\"))]._type" + } + ], + "headers": [ + { + "key": "Authorization", + "value": "Bearer {{ ((dataSource.projects || []).find(p => p.key === object.projectId) || {}).value }}" + } + ], + "postRequestScript": "documentTypeCounts.js" + }, + "ui": [], + "metadata": [ + { + "name": "documentType", + "displayName": "Document Type", + "shape": "string", + "role": "label" + }, + { + "name": "count", + "displayName": "Count", + "shape": "number", + "role": "value" + } + ], + "timeframes": false, + "manualConfigApply": true +} diff --git a/plugins/Sanity/v1/dataStreams/listProjects.json b/plugins/Sanity/v1/dataStreams/listProjects.json new file mode 100644 index 00000000..8badf4ee --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/listProjects.json @@ -0,0 +1,51 @@ +{ + "name": "listProjects", + "displayName": "Projects", + "description": "Lists all Sanity projects the admin token can access. Backs config validation and the project import.", + "tags": ["Project"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "paging": { + "mode": "none" + }, + "expandInnerObjects": false, + "endpointPath": "projects", + "getArgs": [], + "headers": [] + }, + "metadata": [ + { + "name": "id", + "shape": "string", + "displayName": "Project ID" + }, + { + "name": "displayName", + "shape": "string", + "displayName": "Name" + }, + { + "name": "organizationId", + "shape": "string", + "displayName": "Organization ID" + }, + { + "name": "studioHost", + "shape": "string", + "displayName": "Studio host" + }, + { + "name": "createdAt", + "shape": "string", + "displayName": "Created" + }, + { + "pattern": ".*" + } + ], + "timeframes": false, + "visibility": { + "type": "hidden" + } +} diff --git a/plugins/Sanity/v1/dataStreams/members.json b/plugins/Sanity/v1/dataStreams/members.json new file mode 100644 index 00000000..3c89940e --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/members.json @@ -0,0 +1,64 @@ +{ + "name": "members", + "displayName": "Members", + "description": "People with access to a Sanity project, with their roles", + "tags": ["Members"], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Sanity Project" + } + }, + "config": { + "baseUrl": "https://api.sanity.io/v2025-07-11/", + "httpMethod": "get", + "endpointPath": "access/project/{{object.rawId}}/users", + "getArgs": [], + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "100" + }, + "in": { + "realm": "payload", + "path": "nextCursor" + }, + "out": { + "realm": "queryArg", + "path": "nextCursor" + } + }, + "headers": [ + { + "key": "Authorization", + "value": "Bearer {{ ((dataSource.projects || []).find(p => p.key === object.rawId) || {}).value }}" + } + ], + "postRequestScript": "members.js" + }, + "metadata": [ + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "email", + "displayName": "Email", + "shape": "string" + }, + { + "name": "roles", + "displayName": "Roles", + "shape": "string" + }, + { + "pattern": ".*" + } + ], + "timeframes": false +} diff --git a/plugins/Sanity/v1/dataStreams/scripts/changeHistory.js b/plugins/Sanity/v1/dataStreams/scripts/changeHistory.js new file mode 100644 index 00000000..cf138c79 --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/scripts/changeHistory.js @@ -0,0 +1,33 @@ +// Parse NDJSON response from Sanity history/transactions endpoint. +// The endpoint returns one JSON object per line (not a JSON array). +// data will be null/undefined; raw text is in response.body. + +const body = response.body; + +// Defensive: if the handler somehow parsed it already, handle that case +if (Array.isArray(body)) { + result = body.map(function(tx) { + return { + timestamp: tx.timestamp, + transactionId: tx.id, + author: tx.author, + documentCount: Array.isArray(tx.documentIDs) ? tx.documentIDs.length : 0 + }; + }); +} else if (typeof body === 'string') { + result = body + .split('\n') + .map(function(line) { return line.trim(); }) + .filter(function(line) { return line.length > 0; }) + .map(function(line) { return JSON.parse(line); }) + .map(function(tx) { + return { + timestamp: tx.timestamp, + transactionId: tx.id, + author: tx.author, + documentCount: Array.isArray(tx.documentIDs) ? tx.documentIDs.length : 0 + }; + }); +} else { + result = []; +} diff --git a/plugins/Sanity/v1/dataStreams/scripts/datasets.js b/plugins/Sanity/v1/dataStreams/scripts/datasets.js new file mode 100644 index 00000000..17769ec5 --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/scripts/datasets.js @@ -0,0 +1,9 @@ +const object = context.objects[0]; + +result = (data || []).map((d) => ({ + ...d, + datasetId: d.name, + projectName: object?.name, + projectId: object?.rawId, +})); + diff --git a/plugins/Sanity/v1/dataStreams/scripts/documentTypeCounts.js b/plugins/Sanity/v1/dataStreams/scripts/documentTypeCounts.js new file mode 100644 index 00000000..00e3cd92 --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/scripts/documentTypeCounts.js @@ -0,0 +1,7 @@ +// data.result is an array of _type strings for all published documents. +// Use lodash countBy to tally occurrences, then emit one row per type. +const counts = _.countBy(data.result || []); +result = Object.entries(counts).map(([documentType, count]) => ({ + documentType, + count, +})); diff --git a/plugins/Sanity/v1/dataStreams/scripts/members.js b/plugins/Sanity/v1/dataStreams/scripts/members.js new file mode 100644 index 00000000..2d0fb358 --- /dev/null +++ b/plugins/Sanity/v1/dataStreams/scripts/members.js @@ -0,0 +1,15 @@ +// dataStreams/scripts/members.js +// Flatten each member into name, email, roles columns. +// `data` is the parsed response body: { data: [...], nextCursor, totalCount } +// pathToData is ignored when postRequestScript is set, so navigate manually. +const members = (data && data.data) || []; + +result = members.map((user) => ({ + name: user.profile && user.profile.displayName, + email: user.profile && user.profile.email, + roles: (user.memberships || []) + .flatMap((m) => m.roleNames || []) + .join(", "), + sanityUserId: user.sanityUserId, + imageUrl: user.profile && user.profile.imageUrl, +})); diff --git a/plugins/Sanity/v1/defaultContent/manifest.json b/plugins/Sanity/v1/defaultContent/manifest.json new file mode 100644 index 00000000..232ef74a --- /dev/null +++ b/plugins/Sanity/v1/defaultContent/manifest.json @@ -0,0 +1,6 @@ +{ + "items": [ + { "name": "sanityProjects", "type": "dashboard" }, + { "name": "sanityProject", "type": "dashboard" } + ] +} diff --git a/plugins/Sanity/v1/defaultContent/sanityProject.dash.json b/plugins/Sanity/v1/defaultContent/sanityProject.dash.json new file mode 100644 index 00000000..7391f4dc --- /dev/null +++ b/plugins/Sanity/v1/defaultContent/sanityProject.dash.json @@ -0,0 +1,260 @@ +{ + "name": "Project", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Sanity Project]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "f1ecb7e1-1d8e-449c-b39d-521f6b48dfb7", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Project Details", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Sanity Projects]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sanity Project]}}" + }, + "variables": ["{{variables.[Sanity Project]}}"], + "timeframe": "none", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "c52cce1c-efca-48f0-b908-c1d448fadc29", + "x": 1, + "y": 0, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Document Type Counts", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[documentTypeCounts]}}", + "name": "documentTypeCounts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "dataset": "production" + }, + "sort": { + "by": [["count", "desc"]], + "top": 20 + } + }, + "scope": { + "scope": "{{scopes.[Sanity Projects]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sanity Project]}}" + }, + "variables": ["{{variables.[Sanity Project]}}"], + "timeframe": "none", + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "documentType", + "yAxisData": ["count"], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Documents", + "showXAxisLabel": true, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { "type": "auto" } + } + } + } + } + }, + { + "i": "f4d3e130-21b5-4575-bcd9-05460086200b", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Changes", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[changeHistory]}}", + "name": "changeHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "dataset": "production" + } + }, + "scope": { + "scope": "{{scopes.[Sanity Projects]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sanity Project]}}" + }, + "variables": ["{{variables.[Sanity Project]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["timestamp", "author", "documentCount", "transactionId"] + } + } + } + } + }, + { + "i": "4ded0fa6-3ac1-4b0a-af03-123928006951", + "x": 0, + "y": 8, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Datasets", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[datasets]}}", + "name": "datasets", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Sanity Projects]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sanity Project]}}" + }, + "variables": ["{{variables.[Sanity Project]}}"], + "timeframe": "none", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "aclMode"] + } + } + } + } + }, + { + "i": "cb6c1ad8-25f4-4e50-b5c9-619f1cadd6f8", + "x": 2, + "y": 8, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Members", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[members]}}", + "name": "members", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Sanity Projects]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sanity Project]}}" + }, + "variables": ["{{variables.[Sanity Project]}}"], + "timeframe": "none", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "email", "roles"] + } + } + } + } + }, + { + "i": "b36972d8-2fa5-46fa-987b-f811ffa53ff2", + "x": 0, + "y": 12, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Custom GROQ Query", + "description": "Edit the tile to customise the GROQ query and dataset.", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[customQuery]}}", + "name": "customQuery", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "dataset": "production", + "query": "*[0...20]{_id, _type, _updatedAt}", + "perspective": "published" + } + }, + "scope": { + "scope": "{{scopes.[Sanity Projects]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sanity Project]}}" + }, + "variables": ["{{variables.[Sanity Project]}}"], + "timeframe": "none", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + } + } + } + ] + } +} diff --git a/plugins/Sanity/v1/defaultContent/sanityProjects.dash.json b/plugins/Sanity/v1/defaultContent/sanityProjects.dash.json new file mode 100644 index 00000000..02ee7432 --- /dev/null +++ b/plugins/Sanity/v1/defaultContent/sanityProjects.dash.json @@ -0,0 +1,49 @@ +{ + "name": "Projects", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "9e3a54e7-80c9-42df-b8fc-94decb1c5505", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sanity Projects", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Sanity Projects]}}", + "workspace": "{{workspaceId}}" + }, + "timeframe": "none", + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "none", + "linkColumn": "name", + "columns": 4 + } + } + } + } + } + ] + } +} diff --git a/plugins/Sanity/v1/defaultContent/scopes.json b/plugins/Sanity/v1/defaultContent/scopes.json new file mode 100644 index 00000000..99d1ae43 --- /dev/null +++ b/plugins/Sanity/v1/defaultContent/scopes.json @@ -0,0 +1,26 @@ +[ + { + "name": "Sanity Projects", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Sanity Project"] } + }, + "variable": { + "name": "Sanity Project", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Sanity Datasets", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Sanity Dataset"] } + }, + "variable": { + "name": "Sanity Project", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Sanity/v1/docs/README.md b/plugins/Sanity/v1/docs/README.md new file mode 100644 index 00000000..9787395f --- /dev/null +++ b/plugins/Sanity/v1/docs/README.md @@ -0,0 +1,74 @@ +# Sanity + +Bring your [Sanity](https://www.sanity.io) content into SquaredUp. A single plugin instance can cover **many Sanity projects** at once: it lists the projects your admin token can see, and — for each project you supply an API token for — lets you browse datasets, run GROQ queries, break content down by document type, and see a full **history of changes**. + +## What this plugin does + +You give the plugin two things: an **admin token** (used to list projects, datasets and members across your organization) and a set of **per-project API tokens** (used to query each project's content). It then imports: + +- **Each project** your admin token can see, as a `Sanity Project` object you can scope dashboards to and drill into. + +From there you get data streams for: + +- **Change history** — a project/dataset's transaction log: who changed what and when, over any dashboard timeframe. +- **Document type counts** — how many documents of each `_type` live in a dataset. +- **Custom GROQ query** — run any GROQ query against a dataset straight from a tile. +- **Datasets** — a table of a project's datasets and their access modes. +- **Members** (optional) — the people with access to a project and their roles. + +## Why two kinds of token? + +Sanity uses two API surfaces: + +- **Management** (`api.sanity.io`) — listing projects, datasets and members. The plugin uses your **admin token** here. +- **Content** (`.api.sanity.io`) — GROQ queries and history. The plugin uses the **per-project token** matching the project being queried. + +This keeps content access least-privilege: each project's token can only read that project. The query and history tiles work for any project you've added a token for; projects without a token still appear (with their datasets/members via the admin token) but their query/history tiles will show an authorization error. + +## Prerequisites — getting your credentials + +### Admin API token + +1. Sign in to [sanity.io/manage](https://www.sanity.io/manage). +2. Choose an organization-level or project token with enough access to list the projects you care about. A **personal token** (from an admin account) or an organization token works well. To also populate the optional **Members** tile, the token needs member-read access (an Administrator/personal token). +3. If creating a project token: open a project → **API → Tokens → Add API token**, and copy the value (shown once). + +### Project IDs and per-project tokens + +For **each** project you want to query content in: + +1. In [sanity.io/manage](https://www.sanity.io/manage), open the project. +2. Note the **Project ID** — an 8-character string shown on the project page and in its URL (`…/manage/project/`). +3. Go to **API → Tokens → Add API token**, choose the **Viewer** role (read-only), and copy the token value. + +> **Private datasets** require a token; **public** datasets can be queried without one, but a token is still recommended. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +|---|---|---|---| +| **Admin API token** | Token used for management calls (listing projects, datasets, members) | sanity.io/manage → API → Tokens (or a personal/admin token) | Yes | +| **Project API tokens** | A list of **Project ID → API token** pairs, one per project you want to query | Project ID from the project page; Viewer token from API → Tokens | Yes | +| **Content API version** | The dated Sanity API version used for GROQ/history (advanced) | Defaults to `v2025-02-19`; leave as-is unless you need a specific version | No | + +## What gets indexed + +| Object type | Represents | Example | +|---|---|---| +| **Sanity Project** | A project your admin token can see | `My Studio (abc12xyz)` | + +Datasets are **not** indexed as objects (Sanity has no cross-project datasets endpoint). Instead, each project's datasets appear as a table, and you choose a dataset via the **dataset** parameter on the query, history and document-count tiles. + +## Known limitations + +- **No usage or billing metrics.** Sanity's HTTP API exposes no API-request counts, bandwidth, storage, or plan/quota figures. +- **Content tiles need a per-project token.** Query, history and document-count tiles for a project only work if you've added that project's token under **Project API tokens**. +- **Document type counts read document metadata.** The breakdown comes from a GROQ query returning each document's `_type`; on very large datasets it can be slow — scope it to specific types if needed. +- **Change history depth.** The transaction log is fetched per dashboard timeframe with a capped number of transactions per request; very busy windows may be truncated to the most recent transactions. +- **Members tile permissions.** Requires the admin token to have member-read access; otherwise the tile shows an authorization error and nothing else is affected. + +## Useful links + +- [Sanity HTTP API reference](https://www.sanity.io/docs/http-reference) +- [GROQ query language](https://www.sanity.io/docs/groq) +- [Manage your Sanity projects](https://www.sanity.io/manage) diff --git a/plugins/Sanity/v1/icon.svg b/plugins/Sanity/v1/icon.svg new file mode 100644 index 00000000..9f818e07 --- /dev/null +++ b/plugins/Sanity/v1/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/Sanity/v1/indexDefinitions/default.json b/plugins/Sanity/v1/indexDefinitions/default.json new file mode 100644 index 00000000..dc0b2a79 --- /dev/null +++ b/plugins/Sanity/v1/indexDefinitions/default.json @@ -0,0 +1,41 @@ +{ + "steps": [ + { + "name": "projects", + "dataStream": { + "name": "listProjects" + }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "displayName", + "type": { "value": "Sanity Project" }, + "properties": ["organizationId", "studioHost", "createdAt"] + } + }, + { + "name": "datasets", + "dataStream": { + "name": "datasets" + }, + "scope": { + "query": "g.V().has(\"sourceType\", \"Sanity Project\")" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "displayName", + "type": { "value": "Sanity Dataset" }, + "properties": [ + "datasetId", + "aclMode", + "createdAt", + "createdByUserId", + "projectId" + ] + }, + "optional": true, + "dependsOn": ["projects"] + } + ] +} diff --git a/plugins/Sanity/v1/metadata.json b/plugins/Sanity/v1/metadata.json new file mode 100644 index 00000000..d7d169e3 --- /dev/null +++ b/plugins/Sanity/v1/metadata.json @@ -0,0 +1,44 @@ +{ + "name": "sanity", + "displayName": "Sanity", + "version": "1.0.0", + "author": { + "name": "@andrewharris", + "type": "labs" + }, + "description": "Connect one or more Sanity projects: list projects, browse datasets, run GROQ queries, and track a full history of content changes — using an admin token plus per-project API tokens.", + "category": "Database", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["sanity", "cms", "headless", "groq", "content", "dataset"], + "objectTypes": ["Sanity Project"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Sanity/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Sanity/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://api.sanity.io/v2021-06-07/", + "authMode": "none", + "headers": [ + { + "key": "Authorization", + "value": "Bearer {{adminToken}}" + } + ], + "queryArgs": [] + } + } +} diff --git a/plugins/Sanity/v1/ui.json b/plugins/Sanity/v1/ui.json new file mode 100644 index 00000000..a37a3b2a --- /dev/null +++ b/plugins/Sanity/v1/ui.json @@ -0,0 +1,42 @@ +[ + { + "type": "password", + "name": "adminToken", + "label": "Admin API token", + "validation": { + "required": true + }, + "placeholder": "Enter an organization or admin API token", + "help": "Used for management calls — listing projects, datasets and members. Use an organization token or a personal token from an admin account. To populate the optional Members tile this token needs member-read access (Administrator/personal). Create tokens in [sanity.io/manage](https://www.sanity.io/manage) → your project → API → Tokens." + }, + { + "type": "key-value", + "name": "projects", + "label": "Project API tokens", + "displayName": "project", + "verb": "→", + "keyInput": { + "title": "Project ID", + "placeholder": "abc12xyz", + "validation": { + "required": true + } + }, + "valueInput": { + "title": "API token (Viewer)", + "placeholder": "sk...", + "validation": { + "required": true + } + }, + "help": "For each Sanity project you want to query, add its **Project ID** and a project API token (Viewer role). The Project ID is shown on the project page in [sanity.io/manage](https://www.sanity.io/manage); create a token under that project's API → Tokens. Query and history tiles only work for projects listed here." + }, + { + "type": "text", + "name": "apiVersion", + "label": "Content API version", + "defaultValue": "v2025-02-19", + "placeholder": "v2025-02-19", + "help": "Advanced: the dated Sanity API version used for GROQ queries and change history. Leave as the default unless you need a specific version." + } +]