@mekari-officeless/sdk 0.1.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.
- package/README.md +194 -0
- package/package.json +19 -0
- package/src/client.js +84 -0
- package/src/errors.js +8 -0
- package/src/index.js +2 -0
- package/src/table.js +22 -0
- package/src/workflow.js +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @officeless/sdk
|
|
2
|
+
|
|
3
|
+
Official JavaScript SDK for frontend apps (React, Vue, vanilla JS) to interact with Officeless as a serverless backend via `api_v2` workflow triggers.
|
|
4
|
+
|
|
5
|
+
Zero dependencies. ESM-native. Works in any modern browser or Node.js 18+.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @officeless/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Initialization
|
|
18
|
+
|
|
19
|
+
There are two ways to initialize the client.
|
|
20
|
+
|
|
21
|
+
### Mode A: URL Map (recommended after AI porting)
|
|
22
|
+
|
|
23
|
+
When you have an `officeless.config.json` in your project root (auto-generated by the AI Porting Agent), import and pass it directly:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { OfficelessClient } from '@officeless/sdk'
|
|
27
|
+
import config from './officeless.config.json' assert { type: 'json' }
|
|
28
|
+
|
|
29
|
+
const client = OfficelessClient.fromConfig(config)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The config file provides explicit webhook URLs for every workflow, so no guessing is needed.
|
|
33
|
+
|
|
34
|
+
### Mode B: Base URL + convention
|
|
35
|
+
|
|
36
|
+
If you don't have a config file, initialize with just a base URL. Workflow names are mapped to `{baseUrl}/wh/{workflowName}` automatically.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import { OfficelessClient } from '@officeless/sdk'
|
|
40
|
+
|
|
41
|
+
const client = new OfficelessClient({
|
|
42
|
+
baseUrl: 'https://api.officeless.io',
|
|
43
|
+
apiKey: 'olk_live_xxxx', // optional
|
|
44
|
+
projectId: '2BfWnyXYk5mz', // optional
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Constructor options:
|
|
49
|
+
|
|
50
|
+
| Option | Type | Required | Description |
|
|
51
|
+
|--------|------|----------|-------------|
|
|
52
|
+
| `baseUrl` | `string` | yes | Officeless API base URL |
|
|
53
|
+
| `apiKey` | `string` | no | Sent as `X-Officeless-Key` header |
|
|
54
|
+
| `projectId` | `string` | no | Your Officeless project ID |
|
|
55
|
+
| `workflows` | `object` | no | Explicit map of workflow name → `{ url, method }` |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## `OfficelessClient.fromConfig(config)`
|
|
60
|
+
|
|
61
|
+
Loads client settings from a plain object or a parsed `officeless.config.json`.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const client = OfficelessClient.fromConfig({
|
|
65
|
+
officeless: {
|
|
66
|
+
project_id: '2BfWnyXYk5mz',
|
|
67
|
+
base_url: 'https://api.officeless.io',
|
|
68
|
+
api_key: 'olk_live_xxxx',
|
|
69
|
+
workflows: {
|
|
70
|
+
'employee-list': { url: '/app/wh/abc123', method: 'post' },
|
|
71
|
+
'employee-create': { url: '/app/wh/def456', method: 'post' },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Both snake_case (`base_url`, `api_key`, `project_id`) and camelCase (`baseUrl`, `apiKey`, `projectId`) keys are accepted.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Triggering Workflows
|
|
82
|
+
|
|
83
|
+
Use `client.workflow(name)` to get a `WorkflowRef`, then call `.trigger(payload)`.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
const result = await client.workflow('send-welcome-email').trigger({
|
|
87
|
+
userId: 42,
|
|
88
|
+
email: 'user@example.com',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
console.log(result) // data returned by the workflow
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The workflow name must match the key in `officeless.config.json` or the path segment in `{baseUrl}/wh/{name}`.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Table CRUD
|
|
99
|
+
|
|
100
|
+
Use `client.table(name)` to get a `TableRef` for a named table. Each method calls a corresponding workflow: `{tableName}-list`, `{tableName}-create`, `{tableName}-update`, `{tableName}-delete`.
|
|
101
|
+
|
|
102
|
+
### `list(filters?)`
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
const employees = await client.table('employee').list({ department: 'engineering' })
|
|
106
|
+
// calls workflow: employee-list with payload: { filters: { department: 'engineering' } }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `create(data)`
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const newEmployee = await client.table('employee').create({
|
|
113
|
+
name: 'Jane Doe',
|
|
114
|
+
role: 'Engineer',
|
|
115
|
+
})
|
|
116
|
+
// calls workflow: employee-create with payload: { data: { name: 'Jane Doe', role: 'Engineer' } }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `update(id, data)`
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
const updated = await client.table('employee').update('emp_001', { role: 'Senior Engineer' })
|
|
123
|
+
// calls workflow: employee-update with payload: { id: 'emp_001', data: { role: 'Senior Engineer' } }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `delete(id)`
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
await client.table('employee').delete('emp_001')
|
|
130
|
+
// calls workflow: employee-delete with payload: { id: 'emp_001' }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Error Handling
|
|
136
|
+
|
|
137
|
+
All errors thrown by the SDK are instances of `OfficelessError`.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import { OfficelessClient, OfficelessError } from '@officeless/sdk'
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const data = await client.table('employee').list()
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err instanceof OfficelessError) {
|
|
146
|
+
console.error(err.message) // human-readable message
|
|
147
|
+
console.error(err.statusCode) // HTTP status code (e.g. 404, 500)
|
|
148
|
+
console.error(err.response) // raw parsed response body, if available
|
|
149
|
+
} else {
|
|
150
|
+
throw err // re-throw unexpected errors
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`OfficelessError` extends the native `Error` class, so standard `instanceof` checks and stack traces work as expected.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## `officeless.config.json` Format
|
|
160
|
+
|
|
161
|
+
This file is auto-generated by the AI Porting Agent and placed in the frontend project root. It maps each workflow to its webhook URL.
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"officeless": {
|
|
166
|
+
"project_id": "2BfWnyXYk5mz",
|
|
167
|
+
"base_url": "https://api.officeless.io",
|
|
168
|
+
"api_key": "olk_live_xxxx",
|
|
169
|
+
"workflows": {
|
|
170
|
+
"employee-list": { "url": "/app/wh/abc123", "method": "post" },
|
|
171
|
+
"employee-create": { "url": "/app/wh/def456", "method": "post" },
|
|
172
|
+
"employee-update": { "url": "/app/wh/ghi789", "method": "post" },
|
|
173
|
+
"employee-delete": { "url": "/app/wh/jkl012", "method": "post" }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Fields:
|
|
180
|
+
|
|
181
|
+
| Field | Description |
|
|
182
|
+
|-------|-------------|
|
|
183
|
+
| `project_id` | Unique Officeless project identifier |
|
|
184
|
+
| `base_url` | Base API URL for your Officeless instance |
|
|
185
|
+
| `api_key` | Secret key for authenticating requests |
|
|
186
|
+
| `workflows` | Map of workflow name → `{ url, method }`. `url` may be relative (resolved against `base_url`) or absolute. |
|
|
187
|
+
|
|
188
|
+
Commit this file to your repository, but treat `api_key` as a secret — consider loading it from an environment variable in production and excluding the file from version control, or use a build-time substitution step.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mekari-officeless/sdk",
|
|
3
|
+
"private": false,
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"description": "Official Officeless SDK for JS frontends",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "echo 'build placeholder'"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["officeless", "sdk", "nocode"],
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { WorkflowRef } from './workflow.js'
|
|
2
|
+
import { TableRef } from './table.js'
|
|
3
|
+
import { OfficelessError } from './errors.js'
|
|
4
|
+
|
|
5
|
+
export class OfficelessClient {
|
|
6
|
+
/**
|
|
7
|
+
* @param {object} config
|
|
8
|
+
* @param {string} config.baseUrl - Officeless API base URL
|
|
9
|
+
* @param {string} [config.apiKey] - optional API key (sent as X-Officeless-Key header)
|
|
10
|
+
* @param {string} [config.projectId]
|
|
11
|
+
* @param {object} [config.workflows] - map of workflowName → { url, method }
|
|
12
|
+
* e.g. { 'employee-list': { url: '/app/wh/abc123', method: 'post' } }
|
|
13
|
+
* If provided, used directly. If not, uses baseUrl + /wh/ + name convention.
|
|
14
|
+
*/
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
if (!config.baseUrl) throw new OfficelessError('baseUrl is required')
|
|
17
|
+
this._baseUrl = config.baseUrl.replace(/\/$/, '')
|
|
18
|
+
this._apiKey = config.apiKey || null
|
|
19
|
+
this._projectId = config.projectId || null
|
|
20
|
+
this._workflowMap = config.workflows || {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load config from a plain object (e.g. imported officeless.config.json)
|
|
25
|
+
*/
|
|
26
|
+
static fromConfig(config) {
|
|
27
|
+
const c = config?.officeless || config
|
|
28
|
+
return new OfficelessClient({
|
|
29
|
+
baseUrl: c.base_url || c.baseUrl,
|
|
30
|
+
apiKey: c.api_key || c.apiKey,
|
|
31
|
+
projectId: c.project_id || c.projectId,
|
|
32
|
+
workflows: c.workflows || {},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Returns a WorkflowRef for the named workflow */
|
|
37
|
+
workflow(name) {
|
|
38
|
+
return new WorkflowRef(this, name)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns a TableRef for the named table */
|
|
42
|
+
table(name) {
|
|
43
|
+
return new TableRef(this, name)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Internal: resolve workflow URL and POST to it */
|
|
47
|
+
async _callWorkflow(name, payload = {}) {
|
|
48
|
+
const entry = this._workflowMap[name]
|
|
49
|
+
let url
|
|
50
|
+
if (entry && entry.url) {
|
|
51
|
+
// URL map mode: url might be relative or absolute
|
|
52
|
+
url = entry.url.startsWith('http') ? entry.url : this._baseUrl + entry.url
|
|
53
|
+
} else {
|
|
54
|
+
// Convention mode: baseUrl/wh/workflow-name
|
|
55
|
+
url = `${this._baseUrl}/wh/${name}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
59
|
+
if (this._apiKey) headers['X-Officeless-Key'] = this._apiKey
|
|
60
|
+
|
|
61
|
+
const res = await fetch(url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers,
|
|
64
|
+
body: JSON.stringify(payload),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
let body
|
|
68
|
+
try {
|
|
69
|
+
body = await res.json()
|
|
70
|
+
} catch {
|
|
71
|
+
throw new OfficelessError(`Failed to parse response from ${name}`, res.status)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!res.ok || body?.error === true) {
|
|
75
|
+
throw new OfficelessError(
|
|
76
|
+
body?.message || `Request to ${name} failed`,
|
|
77
|
+
res.status,
|
|
78
|
+
body
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return body?.data !== undefined ? body.data : body
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/errors.js
ADDED
package/src/index.js
ADDED
package/src/table.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class TableRef {
|
|
2
|
+
constructor(client, tableName) {
|
|
3
|
+
this._client = client
|
|
4
|
+
this._tableName = tableName
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async list(filters = {}) {
|
|
8
|
+
return this._client._callWorkflow(`${this._tableName}-list`, { filters })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async create(data) {
|
|
12
|
+
return this._client._callWorkflow(`${this._tableName}-create`, { data })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async update(id, data) {
|
|
16
|
+
return this._client._callWorkflow(`${this._tableName}-update`, { id, data })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async delete(id) {
|
|
20
|
+
return this._client._callWorkflow(`${this._tableName}-delete`, { id })
|
|
21
|
+
}
|
|
22
|
+
}
|