@valtrix/sdk 0.0.1 → 0.1.1
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/LICENSE +21 -0
- package/README.md +70 -7
- package/bin/valtrix.mjs +19 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +91 -0
- package/dist/client.d.ts +155 -0
- package/dist/client.js +229 -0
- package/dist/codegen.d.ts +14 -0
- package/dist/codegen.js +103 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +42 -0
- package/dist/http.d.ts +16 -0
- package/dist/http.js +76 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/protocol.d.ts +82 -0
- package/dist/protocol.js +1 -0
- package/package.json +41 -11
- package/index.js +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valtrix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,13 +1,76 @@
|
|
|
1
|
-
# valtrix
|
|
1
|
+
# @valtrix/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed client and codegen CLI for the Valtrix Tables API.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Your data team publishes tables in the Valtrix workspace; this package turns those tables into a typed client for your application. The schema they publish is the contract: column names, types, nullability, and descriptions flow straight into your editor.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
export VALTRIX_API_KEY=vlt_live_... # from your console, under Developers
|
|
11
|
+
npm install @valtrix/sdk
|
|
10
12
|
npx valtrix generate
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
`valtrix generate` reads your published table schemas and
|
|
15
|
+
`valtrix generate` reads your published table schemas and writes `valtrix/client.ts` into your project. Commit it: it is ordinary TypeScript source, and regenerating after a schema change is a reviewable diff.
|
|
16
|
+
|
|
17
|
+
## Reading tables
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { valtrix } from './valtrix/client'
|
|
21
|
+
|
|
22
|
+
const vips = await valtrix.customersClean.findMany({
|
|
23
|
+
where: { lifetime_value: { gte: 10_000 }, email: { not: null } },
|
|
24
|
+
orderBy: { lifetime_value: 'desc' },
|
|
25
|
+
limit: 50,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
for await (const row of valtrix.customersClean.iterate()) {
|
|
29
|
+
process(row)
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Rows come from every organization you hold an active grant for. Pass `org` to narrow to one organization. Filters, ordering, and pagination run server side.
|
|
34
|
+
|
|
35
|
+
## Writing records
|
|
36
|
+
|
|
37
|
+
Writes create records, not rows. They flow through your published transformation like any other source, so they arrive in your tables cleaned, deduped, and quality checked.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
await valtrix.records.customer.upsert({
|
|
41
|
+
org: 'org_acme',
|
|
42
|
+
externalId: 'usr_8f2',
|
|
43
|
+
data: { name: 'Jane Cooper', email: 'JANE@ACME.COM' },
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The response includes the derived table rows, so you read your write already transformed. A row that fails an error-severity quality rule is reported as quarantined instead of landing in the table.
|
|
48
|
+
|
|
49
|
+
## Syncing changes
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
for await (const change of valtrix.customersClean.changes({ cursor })) {
|
|
53
|
+
if (change.type === 'upsert') await warehouse.upsert(change.row)
|
|
54
|
+
else await warehouse.remove(change.recordId)
|
|
55
|
+
cursor = change.cursor
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The feed is ordered and resumable. Store the cursor and pick up where you left off.
|
|
60
|
+
|
|
61
|
+
## Staying in sync with the schema
|
|
62
|
+
|
|
63
|
+
The generated client is pinned to the schema version it was built from. When your data team publishes a new version, the runtime logs a warning, and `valtrix generate --check` exits non-zero, so add it to CI next to your typecheck:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx valtrix generate --check
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
| Variable | Purpose |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `VALTRIX_API_KEY` | Partner API key, required |
|
|
74
|
+
| `VALTRIX_API_URL` | API base URL, defaults to the hosted API |
|
|
75
|
+
|
|
76
|
+
Both can also be passed to `createClient` directly. The client retries rate limits and transient server errors with backoff, and throws typed errors (`ValtrixError`, `ValtrixAuthError`, `ValtrixRateLimitError`, `ValtrixCursorExpiredError`) otherwise.
|
package/bin/valtrix.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const distCli = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'cli.js')
|
|
6
|
+
|
|
7
|
+
let runCli
|
|
8
|
+
try {
|
|
9
|
+
;({ runCli } = await import(distCli))
|
|
10
|
+
} catch {
|
|
11
|
+
process.stderr.write('valtrix: the CLI build is missing. Reinstall the package or run "pnpm build" in valtrix-sdk.\n')
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const code = await runCli(process.argv.slice(2), process.env, {
|
|
16
|
+
log: message => process.stdout.write(`${message}\n`),
|
|
17
|
+
error: message => process.stderr.write(`${message}\n`),
|
|
18
|
+
})
|
|
19
|
+
process.exit(code)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { emitClientSource } from './codegen.js';
|
|
4
|
+
const DEFAULT_OUT = path.join('valtrix', 'client.ts');
|
|
5
|
+
const DEFAULT_BASE_URL = 'https://api.valtrix.vojtadrmota.com';
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const flags = new Map();
|
|
8
|
+
let command = null;
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
const arg = argv[index];
|
|
11
|
+
if (arg.startsWith('--')) {
|
|
12
|
+
const name = arg.slice(2);
|
|
13
|
+
const next = argv[index + 1];
|
|
14
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
15
|
+
flags.set(name, next);
|
|
16
|
+
index += 1;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
flags.set(name, true);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (!command) {
|
|
23
|
+
command = arg;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { command, flags };
|
|
27
|
+
}
|
|
28
|
+
export async function runCli(argv, env, io) {
|
|
29
|
+
const { command, flags } = parseArgs(argv);
|
|
30
|
+
if (!command || command === 'help' || flags.has('help')) {
|
|
31
|
+
io.log('Usage: valtrix generate [--check] [--out <path>] [--url <base url>]');
|
|
32
|
+
io.log('');
|
|
33
|
+
io.log('Reads your published table schemas and writes a typed client.');
|
|
34
|
+
io.log('Environment: VALTRIX_API_KEY (required), VALTRIX_API_URL (optional).');
|
|
35
|
+
return command ? 0 : 1;
|
|
36
|
+
}
|
|
37
|
+
if (command !== 'generate') {
|
|
38
|
+
io.error(`Unknown command "${command}". Run "valtrix help".`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const apiKey = env.VALTRIX_API_KEY;
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
io.error('Set VALTRIX_API_KEY to a key from your Valtrix console (Developers, API Keys).');
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const baseUrl = flags.get('url') ?? env.VALTRIX_API_URL ?? DEFAULT_BASE_URL;
|
|
47
|
+
const outPath = flags.get('out') ?? DEFAULT_OUT;
|
|
48
|
+
const checkOnly = flags.get('check') === true;
|
|
49
|
+
const fetchImpl = io.fetchImpl ?? fetch;
|
|
50
|
+
let schema;
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetchImpl(new URL('/v1/schema', baseUrl).toString(), {
|
|
53
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const body = (await response.json().catch(() => null));
|
|
57
|
+
io.error(`Schema request failed (${response.status}): ${body?.error ?? 'unexpected response'}`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
schema = (await response.json());
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
io.error(`Could not reach ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const generated = emitClientSource(schema);
|
|
67
|
+
if (checkOnly) {
|
|
68
|
+
let existing = null;
|
|
69
|
+
try {
|
|
70
|
+
existing = await readFile(outPath, 'utf8');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
existing = null;
|
|
74
|
+
}
|
|
75
|
+
if (existing === generated.source) {
|
|
76
|
+
io.log(`${outPath} is up to date.`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
io.error(`${outPath} is out of date. Run "valtrix generate" and commit the result.`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
await mkdir(path.dirname(outPath), { recursive: true });
|
|
83
|
+
await writeFile(outPath, generated.source, 'utf8');
|
|
84
|
+
const tableCount = schema.tables.length;
|
|
85
|
+
io.log(`Pulled ${tableCount} ${tableCount === 1 ? 'table' : 'tables'} from your workspace`);
|
|
86
|
+
for (const table of generated.tableProperties) {
|
|
87
|
+
io.log(` valtrix.${table.property} (${table.table})`);
|
|
88
|
+
}
|
|
89
|
+
io.log(`Wrote ${outPath}, fully typed`);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Http } from './http.js';
|
|
2
|
+
import type { ApiSchema, ApiTableOutcome } from './protocol.js';
|
|
3
|
+
export type MetaFields = {
|
|
4
|
+
_record_id: string;
|
|
5
|
+
_org_id: string;
|
|
6
|
+
_synced_at: string;
|
|
7
|
+
};
|
|
8
|
+
export type WhereOperators<V> = {
|
|
9
|
+
eq?: V;
|
|
10
|
+
neq?: V;
|
|
11
|
+
gt?: V;
|
|
12
|
+
gte?: V;
|
|
13
|
+
lt?: V;
|
|
14
|
+
lte?: V;
|
|
15
|
+
in?: V[];
|
|
16
|
+
not?: null;
|
|
17
|
+
};
|
|
18
|
+
export type WhereValue<V> = V | null | WhereOperators<NonNullable<V>>;
|
|
19
|
+
export type Where<Row> = {
|
|
20
|
+
[K in keyof Row]?: WhereValue<Row[K]>;
|
|
21
|
+
};
|
|
22
|
+
export type OrderBy<Row> = {
|
|
23
|
+
[K in keyof Row]?: 'asc' | 'desc';
|
|
24
|
+
};
|
|
25
|
+
export type FindManyQuery<Row> = {
|
|
26
|
+
where?: Where<Row>;
|
|
27
|
+
orderBy?: OrderBy<Row>;
|
|
28
|
+
select?: Array<keyof Row & string>;
|
|
29
|
+
limit?: number;
|
|
30
|
+
cursor?: string;
|
|
31
|
+
org?: string;
|
|
32
|
+
};
|
|
33
|
+
export type RowsPage<Row> = {
|
|
34
|
+
rows: Array<Row & MetaFields>;
|
|
35
|
+
nextCursor: string | null;
|
|
36
|
+
};
|
|
37
|
+
export type TableChange<Row> = {
|
|
38
|
+
type: 'upsert';
|
|
39
|
+
cursor: string;
|
|
40
|
+
occurredAt: string;
|
|
41
|
+
recordId: string;
|
|
42
|
+
orgId: string;
|
|
43
|
+
row: Row & MetaFields;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'delete';
|
|
46
|
+
cursor: string;
|
|
47
|
+
occurredAt: string;
|
|
48
|
+
recordId: string;
|
|
49
|
+
orgId: string;
|
|
50
|
+
row: null;
|
|
51
|
+
};
|
|
52
|
+
export type TableWriteOutcome = ApiTableOutcome;
|
|
53
|
+
export type UpsertResult<Fields> = {
|
|
54
|
+
created: boolean;
|
|
55
|
+
record: {
|
|
56
|
+
id: string;
|
|
57
|
+
externalId: string;
|
|
58
|
+
orgId: string;
|
|
59
|
+
data: Partial<Fields>;
|
|
60
|
+
syncedAt: string;
|
|
61
|
+
};
|
|
62
|
+
tables: TableWriteOutcome[];
|
|
63
|
+
};
|
|
64
|
+
export declare class TableHandle<Row> {
|
|
65
|
+
private readonly http;
|
|
66
|
+
private readonly tableName;
|
|
67
|
+
private readonly pinnedVersion;
|
|
68
|
+
private driftWarned;
|
|
69
|
+
constructor(http: Http, tableName: string, pinnedVersion: string | null);
|
|
70
|
+
private checkDrift;
|
|
71
|
+
private buildQuery;
|
|
72
|
+
page(query?: FindManyQuery<Row>): Promise<RowsPage<Row>>;
|
|
73
|
+
findMany(query?: FindManyQuery<Row>): Promise<Array<Row & MetaFields>>;
|
|
74
|
+
iterate(query?: Omit<FindManyQuery<Row>, 'cursor'>): AsyncGenerator<Row & MetaFields>;
|
|
75
|
+
changes(options?: {
|
|
76
|
+
cursor?: string;
|
|
77
|
+
org?: string;
|
|
78
|
+
limit?: number;
|
|
79
|
+
}): AsyncGenerator<TableChange<Row>>;
|
|
80
|
+
}
|
|
81
|
+
export declare class RecordHandle<Fields> {
|
|
82
|
+
private readonly http;
|
|
83
|
+
private readonly entityType;
|
|
84
|
+
constructor(http: Http, entityType: string);
|
|
85
|
+
upsert(input: {
|
|
86
|
+
org: string;
|
|
87
|
+
externalId: string;
|
|
88
|
+
data: Partial<Fields>;
|
|
89
|
+
}): Promise<UpsertResult<Fields>>;
|
|
90
|
+
get(input: {
|
|
91
|
+
org: string;
|
|
92
|
+
externalId: string;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
record: {
|
|
95
|
+
id: string;
|
|
96
|
+
externalId: string;
|
|
97
|
+
orgId: string;
|
|
98
|
+
data: Partial<Fields>;
|
|
99
|
+
syncedAt: string;
|
|
100
|
+
};
|
|
101
|
+
tables: Array<{
|
|
102
|
+
table: string;
|
|
103
|
+
row: Record<string, unknown> | null;
|
|
104
|
+
}>;
|
|
105
|
+
}>;
|
|
106
|
+
delete(input: {
|
|
107
|
+
org: string;
|
|
108
|
+
externalId: string;
|
|
109
|
+
}): Promise<{
|
|
110
|
+
deleted: boolean;
|
|
111
|
+
tables: TableWriteOutcome[];
|
|
112
|
+
}>;
|
|
113
|
+
}
|
|
114
|
+
export declare class ConnectApi {
|
|
115
|
+
private readonly http;
|
|
116
|
+
constructor(http: Http);
|
|
117
|
+
createSession(input?: {
|
|
118
|
+
orgDisplayName?: string;
|
|
119
|
+
org?: string;
|
|
120
|
+
connector?: string;
|
|
121
|
+
redirectUri?: string;
|
|
122
|
+
scope?: {
|
|
123
|
+
entityTypes: string[];
|
|
124
|
+
};
|
|
125
|
+
}): Promise<{
|
|
126
|
+
connectUrl: string;
|
|
127
|
+
expiresAt: string;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
export type ClientShape = {
|
|
131
|
+
tables: Record<string, object>;
|
|
132
|
+
records: Record<string, object>;
|
|
133
|
+
};
|
|
134
|
+
export type ValtrixClient<S extends ClientShape> = {
|
|
135
|
+
[K in keyof S['tables']]: TableHandle<S['tables'][K]>;
|
|
136
|
+
} & {
|
|
137
|
+
records: {
|
|
138
|
+
[K in keyof S['records']]: RecordHandle<S['records'][K]>;
|
|
139
|
+
};
|
|
140
|
+
connect: ConnectApi;
|
|
141
|
+
schema: () => Promise<ApiSchema>;
|
|
142
|
+
};
|
|
143
|
+
export type CreateClientConfig<S extends ClientShape> = {
|
|
144
|
+
tables: {
|
|
145
|
+
[K in keyof S['tables']]: string;
|
|
146
|
+
};
|
|
147
|
+
records: Array<keyof S['records'] & string>;
|
|
148
|
+
schemaVersions?: Record<string, string>;
|
|
149
|
+
apiKey?: string;
|
|
150
|
+
baseUrl?: string;
|
|
151
|
+
fetch?: typeof fetch;
|
|
152
|
+
onWarning?: (message: string) => void;
|
|
153
|
+
maxRetries?: number;
|
|
154
|
+
};
|
|
155
|
+
export declare function createClient<S extends ClientShape>(config: CreateClientConfig<S>): ValtrixClient<S>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Http } from './http.js';
|
|
2
|
+
function serializeScalar(value) {
|
|
3
|
+
if (typeof value === 'boolean')
|
|
4
|
+
return value ? 'true' : 'false';
|
|
5
|
+
return String(value);
|
|
6
|
+
}
|
|
7
|
+
function whereToParams(where, params) {
|
|
8
|
+
for (const [column, condition] of Object.entries(where)) {
|
|
9
|
+
if (condition === undefined)
|
|
10
|
+
continue;
|
|
11
|
+
if (condition === null) {
|
|
12
|
+
params.append(column, 'is.null');
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (typeof condition === 'object' && !Array.isArray(condition)) {
|
|
16
|
+
const operators = condition;
|
|
17
|
+
for (const [op, value] of Object.entries(operators)) {
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
continue;
|
|
20
|
+
if (op === 'not') {
|
|
21
|
+
params.append(column, 'not.null');
|
|
22
|
+
}
|
|
23
|
+
else if (op === 'in' && Array.isArray(value)) {
|
|
24
|
+
params.append(column, `in.(${value.map(serializeScalar).join(',')})`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
params.append(column, `${op}.${serializeScalar(value)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
params.append(column, `eq.${serializeScalar(condition)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class TableHandle {
|
|
36
|
+
http;
|
|
37
|
+
tableName;
|
|
38
|
+
pinnedVersion;
|
|
39
|
+
driftWarned = false;
|
|
40
|
+
constructor(http, tableName, pinnedVersion) {
|
|
41
|
+
this.http = http;
|
|
42
|
+
this.tableName = tableName;
|
|
43
|
+
this.pinnedVersion = pinnedVersion;
|
|
44
|
+
}
|
|
45
|
+
checkDrift(serverVersion) {
|
|
46
|
+
if (this.driftWarned || !this.pinnedVersion || !serverVersion)
|
|
47
|
+
return;
|
|
48
|
+
if (serverVersion !== this.pinnedVersion) {
|
|
49
|
+
this.driftWarned = true;
|
|
50
|
+
this.http.warn(`valtrix: table "${this.tableName}" is on schema ${serverVersion} but your client was generated against ${this.pinnedVersion}. Run "valtrix generate" to update your types.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
buildQuery(query) {
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
if (query.where)
|
|
56
|
+
whereToParams(query.where, params);
|
|
57
|
+
if (query.orderBy) {
|
|
58
|
+
const [entry] = Object.entries(query.orderBy);
|
|
59
|
+
if (entry)
|
|
60
|
+
params.set('order', `${entry[0]}.${entry[1]}`);
|
|
61
|
+
}
|
|
62
|
+
if (query.select?.length)
|
|
63
|
+
params.set('select', query.select.join(','));
|
|
64
|
+
if (query.limit !== undefined)
|
|
65
|
+
params.set('limit', String(query.limit));
|
|
66
|
+
if (query.cursor)
|
|
67
|
+
params.set('cursor', query.cursor);
|
|
68
|
+
if (query.org)
|
|
69
|
+
params.set('org', query.org);
|
|
70
|
+
return params;
|
|
71
|
+
}
|
|
72
|
+
async page(query = {}) {
|
|
73
|
+
const response = await this.http.request('GET', `/v1/tables/${this.tableName}/rows`, {
|
|
74
|
+
query: this.buildQuery(query),
|
|
75
|
+
});
|
|
76
|
+
this.checkDrift(response.schema_version);
|
|
77
|
+
return { rows: response.rows, nextCursor: response.next_cursor };
|
|
78
|
+
}
|
|
79
|
+
async findMany(query = {}) {
|
|
80
|
+
const { rows } = await this.page(query);
|
|
81
|
+
return rows;
|
|
82
|
+
}
|
|
83
|
+
async *iterate(query = {}) {
|
|
84
|
+
let cursor;
|
|
85
|
+
for (;;) {
|
|
86
|
+
const { rows, nextCursor } = await this.page({ ...query, cursor });
|
|
87
|
+
for (const row of rows)
|
|
88
|
+
yield row;
|
|
89
|
+
if (!nextCursor)
|
|
90
|
+
return;
|
|
91
|
+
cursor = nextCursor;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async *changes(options = {}) {
|
|
95
|
+
let cursor = options.cursor;
|
|
96
|
+
for (;;) {
|
|
97
|
+
const params = new URLSearchParams();
|
|
98
|
+
if (cursor)
|
|
99
|
+
params.set('cursor', cursor);
|
|
100
|
+
if (options.org)
|
|
101
|
+
params.set('org', options.org);
|
|
102
|
+
if (options.limit !== undefined)
|
|
103
|
+
params.set('limit', String(options.limit));
|
|
104
|
+
const response = await this.http.request('GET', `/v1/tables/${this.tableName}/changes`, {
|
|
105
|
+
query: params,
|
|
106
|
+
});
|
|
107
|
+
this.checkDrift(response.schema_version);
|
|
108
|
+
if (!response.changes.length)
|
|
109
|
+
return;
|
|
110
|
+
for (const change of response.changes) {
|
|
111
|
+
if (change.type === 'upsert') {
|
|
112
|
+
yield {
|
|
113
|
+
type: 'upsert',
|
|
114
|
+
cursor: change.cursor,
|
|
115
|
+
occurredAt: change.occurred_at,
|
|
116
|
+
recordId: change.record_id,
|
|
117
|
+
orgId: change.org_id,
|
|
118
|
+
row: { ...(change.row ?? {}), _record_id: change.record_id, _org_id: change.org_id },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
yield {
|
|
123
|
+
type: 'delete',
|
|
124
|
+
cursor: change.cursor,
|
|
125
|
+
occurredAt: change.occurred_at,
|
|
126
|
+
recordId: change.record_id,
|
|
127
|
+
orgId: change.org_id,
|
|
128
|
+
row: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
cursor = response.next_cursor ?? cursor;
|
|
133
|
+
if (!response.next_cursor)
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export class RecordHandle {
|
|
139
|
+
http;
|
|
140
|
+
entityType;
|
|
141
|
+
constructor(http, entityType) {
|
|
142
|
+
this.http = http;
|
|
143
|
+
this.entityType = entityType;
|
|
144
|
+
}
|
|
145
|
+
async upsert(input) {
|
|
146
|
+
const response = await this.http.request('POST', `/v1/records/${this.entityType}`, {
|
|
147
|
+
body: { org: input.org, external_id: input.externalId, data: input.data },
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
created: response.created,
|
|
151
|
+
record: {
|
|
152
|
+
id: response.record.id,
|
|
153
|
+
externalId: response.record.external_id,
|
|
154
|
+
orgId: response.record.org_id,
|
|
155
|
+
data: response.record.data,
|
|
156
|
+
syncedAt: response.record.synced_at,
|
|
157
|
+
},
|
|
158
|
+
tables: response.tables,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async get(input) {
|
|
162
|
+
const params = new URLSearchParams({ org: input.org });
|
|
163
|
+
const response = await this.http.request('GET', `/v1/records/${this.entityType}/${encodeURIComponent(input.externalId)}`, { query: params });
|
|
164
|
+
return {
|
|
165
|
+
record: {
|
|
166
|
+
id: response.record.id,
|
|
167
|
+
externalId: response.record.external_id,
|
|
168
|
+
orgId: response.record.org_id,
|
|
169
|
+
data: response.record.data,
|
|
170
|
+
syncedAt: response.record.synced_at,
|
|
171
|
+
},
|
|
172
|
+
tables: response.tables,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async delete(input) {
|
|
176
|
+
const params = new URLSearchParams({ org: input.org });
|
|
177
|
+
const response = await this.http.request('DELETE', `/v1/records/${this.entityType}/${encodeURIComponent(input.externalId)}`, { query: params });
|
|
178
|
+
return { deleted: response.deleted, tables: response.tables };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export class ConnectApi {
|
|
182
|
+
http;
|
|
183
|
+
constructor(http) {
|
|
184
|
+
this.http = http;
|
|
185
|
+
}
|
|
186
|
+
async createSession(input = {}) {
|
|
187
|
+
const response = await this.http.request('POST', '/v1/connect/sessions', {
|
|
188
|
+
body: {
|
|
189
|
+
org_display_name: input.orgDisplayName,
|
|
190
|
+
org: input.org,
|
|
191
|
+
connector: input.connector,
|
|
192
|
+
redirect_uri: input.redirectUri,
|
|
193
|
+
scope: input.scope,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
return { connectUrl: response.connect_url, expiresAt: response.expires_at };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const DEFAULT_BASE_URL = 'https://api.valtrix.vojtadrmota.com';
|
|
200
|
+
function resolveEnv(name) {
|
|
201
|
+
if (typeof process === 'undefined')
|
|
202
|
+
return undefined;
|
|
203
|
+
return process.env?.[name];
|
|
204
|
+
}
|
|
205
|
+
export function createClient(config) {
|
|
206
|
+
const apiKey = config.apiKey ?? resolveEnv('VALTRIX_API_KEY');
|
|
207
|
+
if (!apiKey) {
|
|
208
|
+
throw new Error('valtrix: set VALTRIX_API_KEY or pass apiKey to createClient.');
|
|
209
|
+
}
|
|
210
|
+
const http = new Http({
|
|
211
|
+
baseUrl: config.baseUrl ?? resolveEnv('VALTRIX_API_URL') ?? DEFAULT_BASE_URL,
|
|
212
|
+
apiKey,
|
|
213
|
+
fetchImpl: config.fetch,
|
|
214
|
+
onWarning: config.onWarning,
|
|
215
|
+
maxRetries: config.maxRetries,
|
|
216
|
+
});
|
|
217
|
+
const client = {};
|
|
218
|
+
for (const [property, tableName] of Object.entries(config.tables)) {
|
|
219
|
+
client[property] = new TableHandle(http, tableName, config.schemaVersions?.[tableName] ?? null);
|
|
220
|
+
}
|
|
221
|
+
const records = {};
|
|
222
|
+
for (const entityType of config.records) {
|
|
223
|
+
records[entityType] = new RecordHandle(http, entityType);
|
|
224
|
+
}
|
|
225
|
+
client.records = records;
|
|
226
|
+
client.connect = new ConnectApi(http);
|
|
227
|
+
client.schema = () => http.request('GET', '/v1/schema');
|
|
228
|
+
return client;
|
|
229
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ApiSchema } from './protocol.js';
|
|
2
|
+
export declare function camelCase(name: string): string;
|
|
3
|
+
export declare function pascalCase(name: string): string;
|
|
4
|
+
export type GeneratedClient = {
|
|
5
|
+
source: string;
|
|
6
|
+
tableProperties: Array<{
|
|
7
|
+
property: string;
|
|
8
|
+
table: string;
|
|
9
|
+
interfaceName: string;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
export declare function emitClientSource(schema: ApiSchema, options?: {
|
|
13
|
+
packageName?: string;
|
|
14
|
+
}): GeneratedClient;
|
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const RESERVED_PROPERTIES = new Set(['records', 'connect', 'schema']);
|
|
2
|
+
export function camelCase(name) {
|
|
3
|
+
return name.replace(/[_-]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase()).replace(/[_-]+$/g, '');
|
|
4
|
+
}
|
|
5
|
+
export function pascalCase(name) {
|
|
6
|
+
const camel = camelCase(name);
|
|
7
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
8
|
+
}
|
|
9
|
+
function tsType(column) {
|
|
10
|
+
switch (column.type) {
|
|
11
|
+
case 'number':
|
|
12
|
+
return 'number';
|
|
13
|
+
case 'boolean':
|
|
14
|
+
return 'boolean';
|
|
15
|
+
case 'json':
|
|
16
|
+
return 'unknown';
|
|
17
|
+
default:
|
|
18
|
+
return 'string';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function docComment(description, extra, indent) {
|
|
22
|
+
const lines = [...(description ? [description] : []), ...extra];
|
|
23
|
+
if (!lines.length)
|
|
24
|
+
return '';
|
|
25
|
+
if (lines.length === 1)
|
|
26
|
+
return `${indent}/** ${lines[0]} */\n`;
|
|
27
|
+
return `${indent}/**\n${lines.map(line => `${indent} * ${line}`).join('\n')}\n${indent} */\n`;
|
|
28
|
+
}
|
|
29
|
+
function columnDoc(column, indent) {
|
|
30
|
+
const extra = [];
|
|
31
|
+
if (column.type === 'date')
|
|
32
|
+
extra.push('ISO 8601 date string.');
|
|
33
|
+
if (column.writable)
|
|
34
|
+
extra.push('Writable through valtrix.records.');
|
|
35
|
+
return docComment(column.description, extra, indent);
|
|
36
|
+
}
|
|
37
|
+
export function emitClientSource(schema, options = {}) {
|
|
38
|
+
const packageName = options.packageName ?? '@valtrix/sdk';
|
|
39
|
+
const tables = [...schema.tables].sort((a, b) => a.name.localeCompare(b.name));
|
|
40
|
+
const records = [...schema.records].sort((a, b) => a.entity_type.localeCompare(b.entity_type));
|
|
41
|
+
const usedProperties = new Set(RESERVED_PROPERTIES);
|
|
42
|
+
const tableProperties = tables.map(table => {
|
|
43
|
+
let property = camelCase(table.name);
|
|
44
|
+
if (!property)
|
|
45
|
+
property = 'table';
|
|
46
|
+
while (usedProperties.has(property))
|
|
47
|
+
property = `${property}Table`;
|
|
48
|
+
usedProperties.add(property);
|
|
49
|
+
return { property, table: table.name, interfaceName: pascalCase(table.name) };
|
|
50
|
+
});
|
|
51
|
+
const parts = [];
|
|
52
|
+
parts.push(`import { createClient } from '${packageName}'`);
|
|
53
|
+
parts.push('');
|
|
54
|
+
tables.forEach((table, index) => {
|
|
55
|
+
const { interfaceName } = tableProperties[index];
|
|
56
|
+
const header = docComment(`Table "${table.name}" (${table.entity_type}), schema ${table.schema_version ?? 'unpublished'}.`, [], '');
|
|
57
|
+
parts.push(`${header}export interface ${interfaceName} {`);
|
|
58
|
+
for (const column of table.columns) {
|
|
59
|
+
const doc = columnDoc(column, ' ');
|
|
60
|
+
const nullable = column.nullable ? ' | null' : '';
|
|
61
|
+
parts.push(`${doc} ${column.key}: ${tsType(column)}${nullable}`);
|
|
62
|
+
}
|
|
63
|
+
parts.push('}');
|
|
64
|
+
parts.push('');
|
|
65
|
+
});
|
|
66
|
+
records.forEach(record => {
|
|
67
|
+
const interfaceName = `${pascalCase(record.entity_type)}Record`;
|
|
68
|
+
parts.push(`${docComment(`Writable fields for the "${record.entity_type}" entity type.`, [], '')}export interface ${interfaceName} {`);
|
|
69
|
+
for (const column of record.columns) {
|
|
70
|
+
const doc = docComment(column.description, column.type === 'date' ? ['ISO 8601 date string.'] : [], ' ');
|
|
71
|
+
parts.push(`${doc} ${column.key}?: ${tsType({ type: column.type })} | null`);
|
|
72
|
+
}
|
|
73
|
+
parts.push('}');
|
|
74
|
+
parts.push('');
|
|
75
|
+
});
|
|
76
|
+
parts.push('export const valtrix = createClient<{');
|
|
77
|
+
parts.push(' tables: {');
|
|
78
|
+
tableProperties.forEach(({ property, interfaceName }) => {
|
|
79
|
+
parts.push(` ${property}: ${interfaceName}`);
|
|
80
|
+
});
|
|
81
|
+
parts.push(' }');
|
|
82
|
+
parts.push(' records: {');
|
|
83
|
+
records.forEach(record => {
|
|
84
|
+
parts.push(` ${record.entity_type}: ${pascalCase(record.entity_type)}Record`);
|
|
85
|
+
});
|
|
86
|
+
parts.push(' }');
|
|
87
|
+
parts.push('}>({');
|
|
88
|
+
parts.push(' tables: {');
|
|
89
|
+
tableProperties.forEach(({ property, table }) => {
|
|
90
|
+
parts.push(` ${property}: '${table}',`);
|
|
91
|
+
});
|
|
92
|
+
parts.push(' },');
|
|
93
|
+
parts.push(` records: [${records.map(record => `'${record.entity_type}'`).join(', ')}],`);
|
|
94
|
+
parts.push(' schemaVersions: {');
|
|
95
|
+
tables.forEach(table => {
|
|
96
|
+
if (table.schema_version)
|
|
97
|
+
parts.push(` ${JSON.stringify(table.name)}: '${table.schema_version}',`);
|
|
98
|
+
});
|
|
99
|
+
parts.push(' },');
|
|
100
|
+
parts.push('})');
|
|
101
|
+
parts.push('');
|
|
102
|
+
return { source: parts.join('\n'), tableProperties };
|
|
103
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type FieldIssue = {
|
|
2
|
+
field: string;
|
|
3
|
+
error: string;
|
|
4
|
+
};
|
|
5
|
+
export declare class ValtrixError extends Error {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly fields: FieldIssue[];
|
|
9
|
+
constructor(status: number, code: string, message: string, fields?: FieldIssue[]);
|
|
10
|
+
}
|
|
11
|
+
export declare class ValtrixAuthError extends ValtrixError {
|
|
12
|
+
constructor(status: number, code: string, message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class ValtrixRateLimitError extends ValtrixError {
|
|
15
|
+
readonly retryAfterSeconds: number;
|
|
16
|
+
constructor(message: string, retryAfterSeconds: number);
|
|
17
|
+
}
|
|
18
|
+
export declare class ValtrixCursorExpiredError extends ValtrixError {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
export declare function errorFromResponse(status: number, body: unknown): ValtrixError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class ValtrixError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
fields;
|
|
5
|
+
constructor(status, code, message, fields = []) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ValtrixError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.fields = fields;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class ValtrixAuthError extends ValtrixError {
|
|
14
|
+
constructor(status, code, message) {
|
|
15
|
+
super(status, code, message);
|
|
16
|
+
this.name = 'ValtrixAuthError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class ValtrixRateLimitError extends ValtrixError {
|
|
20
|
+
retryAfterSeconds;
|
|
21
|
+
constructor(message, retryAfterSeconds) {
|
|
22
|
+
super(429, 'rate_limited', message);
|
|
23
|
+
this.name = 'ValtrixRateLimitError';
|
|
24
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ValtrixCursorExpiredError extends ValtrixError {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(410, 'cursor_expired', message);
|
|
30
|
+
this.name = 'ValtrixCursorExpiredError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function errorFromResponse(status, body) {
|
|
34
|
+
const parsed = (typeof body === 'object' && body !== null ? body : {});
|
|
35
|
+
const code = parsed.code ?? 'unknown_error';
|
|
36
|
+
const message = parsed.error ?? `Request failed with status ${status}.`;
|
|
37
|
+
if (status === 401 || status === 403)
|
|
38
|
+
return new ValtrixAuthError(status, code, message);
|
|
39
|
+
if (status === 410 && code === 'cursor_expired')
|
|
40
|
+
return new ValtrixCursorExpiredError(message);
|
|
41
|
+
return new ValtrixError(status, code, message, parsed.fields ?? []);
|
|
42
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type HttpConfig = {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
fetchImpl?: typeof fetch;
|
|
5
|
+
maxRetries?: number;
|
|
6
|
+
onWarning?: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare class Http {
|
|
9
|
+
private readonly config;
|
|
10
|
+
constructor(config: HttpConfig);
|
|
11
|
+
warn(message: string): void;
|
|
12
|
+
request<T>(method: string, path: string, options?: {
|
|
13
|
+
query?: URLSearchParams;
|
|
14
|
+
body?: unknown;
|
|
15
|
+
}): Promise<T>;
|
|
16
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { errorFromResponse, ValtrixError, ValtrixRateLimitError } from './errors.js';
|
|
2
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
3
|
+
function sleep(ms) {
|
|
4
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
5
|
+
}
|
|
6
|
+
export class Http {
|
|
7
|
+
config;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
}
|
|
11
|
+
warn(message) {
|
|
12
|
+
const handler = this.config.onWarning ?? ((text) => console.warn(text));
|
|
13
|
+
handler(message);
|
|
14
|
+
}
|
|
15
|
+
async request(method, path, options = {}) {
|
|
16
|
+
const fetchImpl = this.config.fetchImpl ?? fetch;
|
|
17
|
+
const maxRetries = this.config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
18
|
+
const url = new URL(path, this.config.baseUrl);
|
|
19
|
+
if (options.query) {
|
|
20
|
+
for (const [key, value] of options.query)
|
|
21
|
+
url.searchParams.append(key, value);
|
|
22
|
+
}
|
|
23
|
+
let lastError = null;
|
|
24
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetchImpl(url.toString(), {
|
|
28
|
+
method,
|
|
29
|
+
headers: {
|
|
30
|
+
authorization: `Bearer ${this.config.apiKey}`,
|
|
31
|
+
...(options.body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
32
|
+
},
|
|
33
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
lastError = error;
|
|
38
|
+
if (attempt < maxRetries) {
|
|
39
|
+
await sleep(250 * 2 ** attempt);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
throw new ValtrixError(0, 'network_error', error instanceof Error ? error.message : String(error));
|
|
43
|
+
}
|
|
44
|
+
if (response.status === 429) {
|
|
45
|
+
const retryAfter = Number(response.headers.get('retry-after') ?? '1');
|
|
46
|
+
const waitSeconds = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : 1;
|
|
47
|
+
if (attempt < maxRetries) {
|
|
48
|
+
await sleep(Math.min(waitSeconds, 30) * 1000);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
throw new ValtrixRateLimitError('Rate limit exceeded.', waitSeconds);
|
|
52
|
+
}
|
|
53
|
+
if (response.status >= 500) {
|
|
54
|
+
lastError = errorFromResponse(response.status, await safeJson(response));
|
|
55
|
+
if (attempt < maxRetries) {
|
|
56
|
+
await sleep(250 * 2 ** attempt);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
throw lastError;
|
|
60
|
+
}
|
|
61
|
+
const body = await safeJson(response);
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
throw errorFromResponse(response.status, body);
|
|
64
|
+
return body;
|
|
65
|
+
}
|
|
66
|
+
throw lastError instanceof Error ? lastError : new ValtrixError(0, 'network_error', 'Request failed.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function safeJson(response) {
|
|
70
|
+
try {
|
|
71
|
+
return await response.json();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ConnectApi, createClient, RecordHandle, TableHandle, } from './client.js';
|
|
2
|
+
export type { ClientShape, CreateClientConfig, FindManyQuery, MetaFields, OrderBy, RowsPage, TableChange, TableWriteOutcome, UpsertResult, ValtrixClient, Where, WhereOperators, WhereValue, } from './client.js';
|
|
3
|
+
export { errorFromResponse, ValtrixAuthError, ValtrixCursorExpiredError, ValtrixError, ValtrixRateLimitError, } from './errors.js';
|
|
4
|
+
export type { FieldIssue } from './errors.js';
|
|
5
|
+
export { camelCase, emitClientSource, pascalCase } from './codegen.js';
|
|
6
|
+
export type { GeneratedClient } from './codegen.js';
|
|
7
|
+
export { runCli } from './cli.js';
|
|
8
|
+
export type { ApiChange, ApiChangesResponse, ApiColumn, ApiConnectSessionResponse, ApiRecordType, ApiRowsResponse, ApiSchema, ApiTable, ApiUpsertResponse, } from './protocol.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ConnectApi, createClient, RecordHandle, TableHandle, } from './client.js';
|
|
2
|
+
export { errorFromResponse, ValtrixAuthError, ValtrixCursorExpiredError, ValtrixError, ValtrixRateLimitError, } from './errors.js';
|
|
3
|
+
export { camelCase, emitClientSource, pascalCase } from './codegen.js';
|
|
4
|
+
export { runCli } from './cli.js';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type ApiColumn = {
|
|
2
|
+
key: string;
|
|
3
|
+
type: 'string' | 'number' | 'boolean' | 'date' | 'json';
|
|
4
|
+
nullable: boolean;
|
|
5
|
+
description: string;
|
|
6
|
+
writable: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type ApiTable = {
|
|
9
|
+
name: string;
|
|
10
|
+
entity_type: string;
|
|
11
|
+
status: string;
|
|
12
|
+
row_count: number;
|
|
13
|
+
last_built_at: string | null;
|
|
14
|
+
schema_version: string | null;
|
|
15
|
+
columns: ApiColumn[];
|
|
16
|
+
};
|
|
17
|
+
export type ApiRecordType = {
|
|
18
|
+
entity_type: string;
|
|
19
|
+
label: string;
|
|
20
|
+
title_key: string;
|
|
21
|
+
columns: Array<{
|
|
22
|
+
key: string;
|
|
23
|
+
type: string;
|
|
24
|
+
description: string;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
export type ApiSchema = {
|
|
28
|
+
tables: ApiTable[];
|
|
29
|
+
records: ApiRecordType[];
|
|
30
|
+
};
|
|
31
|
+
export type ApiRowsResponse = {
|
|
32
|
+
rows: Array<Record<string, unknown>>;
|
|
33
|
+
next_cursor: string | null;
|
|
34
|
+
schema_version: string | null;
|
|
35
|
+
};
|
|
36
|
+
export type ApiChange = {
|
|
37
|
+
type: 'upsert' | 'delete';
|
|
38
|
+
cursor: string;
|
|
39
|
+
occurred_at: string;
|
|
40
|
+
record_id: string;
|
|
41
|
+
org_id: string;
|
|
42
|
+
row: Record<string, unknown> | null;
|
|
43
|
+
};
|
|
44
|
+
export type ApiChangesResponse = {
|
|
45
|
+
changes: ApiChange[];
|
|
46
|
+
next_cursor: string | null;
|
|
47
|
+
schema_version: string | null;
|
|
48
|
+
};
|
|
49
|
+
export type ApiTableOutcome = {
|
|
50
|
+
table: string;
|
|
51
|
+
status: 'applied' | 'removed' | 'quarantined' | 'filtered' | 'pending';
|
|
52
|
+
row: Record<string, unknown> | null;
|
|
53
|
+
quarantined_by: string[];
|
|
54
|
+
};
|
|
55
|
+
export type ApiRecord = {
|
|
56
|
+
id: string;
|
|
57
|
+
entity_type: string;
|
|
58
|
+
external_id: string;
|
|
59
|
+
org_id: string;
|
|
60
|
+
data: Record<string, unknown>;
|
|
61
|
+
synced_at: string;
|
|
62
|
+
};
|
|
63
|
+
export type ApiUpsertResponse = {
|
|
64
|
+
created: boolean;
|
|
65
|
+
record: ApiRecord;
|
|
66
|
+
tables: ApiTableOutcome[];
|
|
67
|
+
};
|
|
68
|
+
export type ApiDeleteResponse = {
|
|
69
|
+
deleted: boolean;
|
|
70
|
+
tables: ApiTableOutcome[];
|
|
71
|
+
};
|
|
72
|
+
export type ApiRecordResponse = {
|
|
73
|
+
record: ApiRecord;
|
|
74
|
+
tables: Array<{
|
|
75
|
+
table: string;
|
|
76
|
+
row: Record<string, unknown> | null;
|
|
77
|
+
}>;
|
|
78
|
+
};
|
|
79
|
+
export type ApiConnectSessionResponse = {
|
|
80
|
+
connect_url: string;
|
|
81
|
+
expires_at: string;
|
|
82
|
+
};
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valtrix/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Typed client and codegen CLI for the Valtrix Tables API. Run valtrix generate to get a client typed against your published tables.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"valtrix": "./bin/valtrix.mjs"
|
|
6
17
|
},
|
|
7
|
-
"description": "Valtrix SDK - typed client and codegen CLI for the Valtrix data plane (coming soon)",
|
|
8
|
-
"license": "UNLICENSED",
|
|
9
|
-
"main": "index.js",
|
|
10
18
|
"files": [
|
|
11
|
-
"
|
|
12
|
-
"
|
|
19
|
+
"bin",
|
|
20
|
+
"dist"
|
|
13
21
|
],
|
|
14
22
|
"keywords": [
|
|
15
|
-
"valtrix"
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
"valtrix",
|
|
24
|
+
"sdk",
|
|
25
|
+
"typed-client",
|
|
26
|
+
"codegen",
|
|
27
|
+
"data-api"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.17"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.0.8",
|
|
37
|
+
"typescript": "^5.9.3",
|
|
38
|
+
"vitest": "^4.0.17",
|
|
39
|
+
"@valtrix/tsconfig": "1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest",
|
|
45
|
+
"test:run": "vitest run"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {};
|