@empyria/restate 0.1.15 → 0.1.17

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/lib/Admin.js CHANGED
@@ -127,6 +127,35 @@ export async function listServices({ restateAdminURL }) {
127
127
  return data.services // Array of { name, handlers, ty, deployment_id, ... }
128
128
  }
129
129
 
130
+ /**
131
+ * Runs a raw SQL query against Restate's own introspection API (`POST {adminURL}/query`) — the
132
+ * `sys_invocation`/`state`/`sys_promise`/... tables Restate exposes specifically so operators
133
+ * and tooling can inspect what's running without an official client for it (see this file's own
134
+ * "No official Admin API client" header note — same "fetch the REST endpoint directly" posture
135
+ * as every other function here; this is simply the one REST endpoint none of them touch yet).
136
+ * The endpoint takes a single JSON string, no parameter binding — build `sql` with any
137
+ * caller-supplied values already escaped (single quotes doubled) before calling this.
138
+ * @param {{restateAdminURL: string, sql: string}} params
139
+ * @returns {Promise<Record<string, unknown>[]>}
140
+ */
141
+ export async function queryRestate({ restateAdminURL, sql }) {
142
+ const response = await fetch(`${restateAdminURL}/query`, {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
145
+ body: JSON.stringify({ query: sql }),
146
+ })
147
+ if (!response.ok) {
148
+ const body = await response.text().catch(() => '')
149
+ throw new restate.TerminalError(
150
+ `Restate SQL query failed: ${response.status} ${response.statusText} — ${body.slice(0, 500)}`,
151
+ { errorCode: ERROR_INFRASTRUCTURE },
152
+ )
153
+ }
154
+ /** @type {{rows: Record<string, unknown>[]}} */
155
+ const { rows } = await response.json()
156
+ return rows
157
+ }
158
+
130
159
  /**
131
160
  * Force-deletes a deployment from Restate, draining any in-flight invocations.
132
161
  * @param {string} restateAdminURL
@@ -691,6 +720,7 @@ export const sendMessageAsynSafe = async ({ restateURL, callerParams, timeout })
691
720
 
692
721
  /**
693
722
  * @typedef {Object} RestateAdmin
723
+ * @property {(sql: string) => Promise<Record<string, unknown>[]>} query
694
724
  * @property {() => Promise<ServiceMetadata[]>} listServices
695
725
  * @property {(params: {name: string}) => Promise<ServiceMetadata>} service
696
726
  * @property {(params: {name: string}) => Promise<HandlerMetadata[]>} listHandlers
@@ -714,6 +744,8 @@ export const sendMessageAsynSafe = async ({ restateURL, callerParams, timeout })
714
744
  */
715
745
  export function createRestateAdmin({ restateAdminURL, restateURL }) {
716
746
  return {
747
+ query: (sql) => queryRestate({ restateAdminURL, sql }),
748
+
717
749
  // Services
718
750
  listServices: () => listServices({ restateAdminURL }),
719
751
  service: ({ name }) => service({ restateAdminURL, name }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empyria/restate",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Restate.dev helpers for the Empyria nanoservice framework",
5
5
  "license": "MIT",
6
6
  "author": "Imre Fazekas <imre.fazekas@icloud.com>",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@empyria/classification": "0.1.7",
28
- "@empyria/common": "0.3.2",
28
+ "@empyria/common": "0.3.3",
29
29
  "@restatedev/pubsub": "0.3.2",
30
30
  "@restatedev/pubsub-client": "0.3.2",
31
31
  "@restatedev/restate-sdk": "1.17.0",
@@ -4,6 +4,7 @@ import {
4
4
  startSubWorkflow,
5
5
  callSubWorkflow,
6
6
  listServices,
7
+ queryRestate,
7
8
  deleteDeployment,
8
9
  service,
9
10
  listHandlers,
@@ -173,6 +174,27 @@ describe('listServices', () => {
173
174
  })
174
175
  })
175
176
 
177
+ describe('queryRestate', () => {
178
+ test('POSTs {query: sql} to /query and returns the rows', async () => {
179
+ const rows = [{ target_service_key: 'CA0001', status: 'suspended' }]
180
+ globalThis.fetch = mock(async (url, init) => {
181
+ expect(String(url)).toBe('http://admin/query')
182
+ expect(JSON.parse(init.body)).toEqual({ query: 'SELECT 1' })
183
+ return jsonResponse({ rows })
184
+ })
185
+ expect(await queryRestate({ restateAdminURL: 'http://admin', sql: 'SELECT 1' })).toEqual(
186
+ rows,
187
+ )
188
+ })
189
+
190
+ test('throws a TerminalError on a failed query', async () => {
191
+ globalThis.fetch = mock(async () => jsonResponse(null, false))
192
+ await expect(
193
+ queryRestate({ restateAdminURL: 'http://admin', sql: 'SELECT 1' }),
194
+ ).rejects.toThrow(restate.TerminalError)
195
+ })
196
+ })
197
+
176
198
  describe('deleteDeployment', () => {
177
199
  test('resolves on success', async () => {
178
200
  globalThis.fetch = mock(async () => jsonResponse({}))
@@ -486,4 +508,18 @@ describe('createRestateAdmin', () => {
486
508
  })
487
509
  expect(await admin.listServices()).toEqual(servicesPayload.services)
488
510
  })
511
+
512
+ test('query() binds restateAdminURL and forwards the raw SQL', async () => {
513
+ const rows = [{ status: 'completed' }]
514
+ globalThis.fetch = mock(async (url, init) => {
515
+ expect(String(url)).toBe('http://admin/query')
516
+ expect(JSON.parse(init.body)).toEqual({ query: 'SELECT status FROM sys_invocation' })
517
+ return jsonResponse({ rows })
518
+ })
519
+ const admin = createRestateAdmin({
520
+ restateAdminURL: 'http://admin',
521
+ restateURL: 'http://ingress',
522
+ })
523
+ expect(await admin.query('SELECT status FROM sys_invocation')).toEqual(rows)
524
+ })
489
525
  })