lingcode-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/index.d.ts +166 -0
- package/index.mjs +9 -0
- package/lingcode-sw.js +46 -0
- package/lingcode-v1.js +343 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# lingcode-js
|
|
2
|
+
|
|
3
|
+
Official client SDK for a [LingCode](https://lingcode.dev) managed backend — database, auth, realtime, storage, serverless functions, vector search, and push, in one zero-dependency client.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install lingcode-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or drop it in with a `<script>` tag (no build step):
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script src="https://lingcode.dev/sdk/lingcode-v1.js"></script>
|
|
15
|
+
<script>
|
|
16
|
+
const lingcode = LingCode.createClient(BACKEND_URL, ANON_KEY);
|
|
17
|
+
</script>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> In apps built with LingCode `/try`, a ready `window.lingcode` is **already injected** — you can skip `createClient` entirely.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { createClient } from "lingcode-js";
|
|
26
|
+
|
|
27
|
+
const lingcode = createClient(
|
|
28
|
+
"https://lingcode.dev/api/cloud/be/<your-backend-id>",
|
|
29
|
+
"<your-anon-key>"
|
|
30
|
+
);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Database
|
|
34
|
+
|
|
35
|
+
Supabase-style query builder — filters first, terminal op last.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// Select
|
|
39
|
+
const { data, error } = await lingcode
|
|
40
|
+
.from("todos")
|
|
41
|
+
.eq("done", false)
|
|
42
|
+
.order("created_at", { ascending: false })
|
|
43
|
+
.limit(50)
|
|
44
|
+
.select();
|
|
45
|
+
|
|
46
|
+
// Insert
|
|
47
|
+
await lingcode.from("todos").insert({ title: "Buy milk" });
|
|
48
|
+
|
|
49
|
+
// Update / delete (a filter is REQUIRED)
|
|
50
|
+
await lingcode.from("todos").eq("id", 1).update({ done: true });
|
|
51
|
+
await lingcode.from("todos").eq("id", 1).delete();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Filters: `.eq .neq .gt .gte .lt .lte .like .ilike .in(col, [...]) .is(col, null | "not_null") .match({ ... })`.
|
|
55
|
+
|
|
56
|
+
## Realtime
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
const off = lingcode.from("todos").subscribe(({ type, row }) => {
|
|
60
|
+
// type: "INSERT" | "UPDATE" | "DELETE" — patch your UI
|
|
61
|
+
});
|
|
62
|
+
// later: off();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Server-side RLS means a signed-in user only ever receives their own rows.
|
|
66
|
+
|
|
67
|
+
## Auth
|
|
68
|
+
|
|
69
|
+
The SDK persists the session and auto-attaches it to later calls.
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
await lingcode.auth.signUp({ email, password });
|
|
73
|
+
await lingcode.auth.signIn({ email, password });
|
|
74
|
+
|
|
75
|
+
// Passwordless (the SDK finalizes the link/redirect automatically)
|
|
76
|
+
await lingcode.auth.sendMagicLink({ email });
|
|
77
|
+
await lingcode.ready; // wait for redirect-session consumption on load
|
|
78
|
+
lingcode.auth.getUser(); // { id, email } | null
|
|
79
|
+
|
|
80
|
+
// Social (only render buttons whose provider is available)
|
|
81
|
+
const providers = await lingcode.auth.getProviders();
|
|
82
|
+
if (providers.google?.available) lingcode.auth.signInWithOAuth("google");
|
|
83
|
+
|
|
84
|
+
// Email code
|
|
85
|
+
await lingcode.auth.sendOtp({ email });
|
|
86
|
+
await lingcode.auth.verifyOtp({ email, code });
|
|
87
|
+
|
|
88
|
+
await lingcode.auth.signOut();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Storage
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const { data } = await lingcode.storage.from("public").upload("avatars/me.png", file);
|
|
95
|
+
const url = lingcode.storage.from("public").getPublicUrl("avatars/me.png");
|
|
96
|
+
await lingcode.storage.from("public").remove("avatars/me.png");
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Functions
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
const { data } = await lingcode.functions.invoke("send-email", { to, subject, html });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Vector search
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const { data } = await lingcode.vector.search({
|
|
109
|
+
table: "docs", column: "embedding", embedding: queryVec, limit: 5, metric: "cosine",
|
|
110
|
+
});
|
|
111
|
+
// Optional managed embeddings:
|
|
112
|
+
const { data: e } = await lingcode.vector.embed("some text"); // e.embedding
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Push notifications (Web Push)
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
await lingcode.push.subscribe(); // registers the service worker + subscribes
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The owner sends notifications from the LingCode Cloud console (or the backend API). For apps served on their own origin, host the service worker at your origin and pass `{ serviceWorker: "/lingcode-sw.js" }`.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Type definitions for lingcode-js
|
|
2
|
+
// Project: https://lingcode.dev
|
|
3
|
+
|
|
4
|
+
export interface LingCodeError extends Error {
|
|
5
|
+
/** Server error code, e.g. "where_required", "object_not_found", or null. */
|
|
6
|
+
code: string | null;
|
|
7
|
+
/** HTTP status (0 for client-side errors). */
|
|
8
|
+
status: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Supabase-style result envelope: check `error` before using `data`. */
|
|
12
|
+
export interface Result<T = any> {
|
|
13
|
+
data: T | null;
|
|
14
|
+
error: LingCodeError | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface User {
|
|
18
|
+
id: string | null;
|
|
19
|
+
email: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Session {
|
|
23
|
+
user: User | null;
|
|
24
|
+
token: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A live row-change event delivered to `.subscribe()`. */
|
|
28
|
+
export interface ChangeEvent<T = any> {
|
|
29
|
+
table: string;
|
|
30
|
+
type: "INSERT" | "UPDATE" | "DELETE";
|
|
31
|
+
row: T;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Cancels a realtime subscription. */
|
|
35
|
+
export type Unsubscribe = () => void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Chainable query builder. Add filters (`.eq`, `.in`, …) and modifiers
|
|
39
|
+
* (`.order`, `.limit`) before a terminal op (`.select`, `.insert`,
|
|
40
|
+
* `.update`, `.delete`). `.update()`/`.delete()` REQUIRE a filter.
|
|
41
|
+
*/
|
|
42
|
+
export interface QueryBuilder<T = any> {
|
|
43
|
+
eq(column: string, value: any): this;
|
|
44
|
+
neq(column: string, value: any): this;
|
|
45
|
+
gt(column: string, value: any): this;
|
|
46
|
+
gte(column: string, value: any): this;
|
|
47
|
+
lt(column: string, value: any): this;
|
|
48
|
+
lte(column: string, value: any): this;
|
|
49
|
+
like(column: string, value: string): this;
|
|
50
|
+
ilike(column: string, value: string): this;
|
|
51
|
+
in(column: string, values: any[]): this;
|
|
52
|
+
/** `.is(col, null)` → IS NULL; `.is(col, "not_null")` → IS NOT NULL. */
|
|
53
|
+
is(column: string, value: null | "not_null"): this;
|
|
54
|
+
/** Merge several equality filters at once. */
|
|
55
|
+
match(filters: Record<string, any>): this;
|
|
56
|
+
order(column: string, opts?: { ascending?: boolean }): this;
|
|
57
|
+
limit(n: number): this;
|
|
58
|
+
range(from: number, to: number): this;
|
|
59
|
+
|
|
60
|
+
select(): Promise<Result<T[]>>;
|
|
61
|
+
insert(row: Partial<T> | Partial<T>[]): Promise<Result<T[]>>;
|
|
62
|
+
/** Requires a filter (.eq/.match). */
|
|
63
|
+
update(patch: Partial<T>): Promise<Result<T[]>>;
|
|
64
|
+
/** Requires a filter (.eq/.match). */
|
|
65
|
+
delete(): Promise<Result<T[]>>;
|
|
66
|
+
|
|
67
|
+
/** Subscribe to live INSERT/UPDATE/DELETE events (RLS-filtered). */
|
|
68
|
+
subscribe(
|
|
69
|
+
onChange: (event: ChangeEvent<T>) => void,
|
|
70
|
+
onError?: (e: Event) => void
|
|
71
|
+
): Unsubscribe;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ProviderInfo {
|
|
75
|
+
available: boolean;
|
|
76
|
+
source?: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface AuthApi {
|
|
80
|
+
signUp(creds: { email: string; password: string }): Promise<Result<Session>>;
|
|
81
|
+
signIn(creds: { email: string; password: string }): Promise<Result<Session>>;
|
|
82
|
+
signInWithPassword(creds: { email: string; password: string }): Promise<Result<Session>>;
|
|
83
|
+
/** Top-level navigation to the provider; on return the session is auto-stored. */
|
|
84
|
+
signInWithOAuth(provider: "google" | "github" | "apple" | string, opts?: { redirectTo?: string }): void;
|
|
85
|
+
/** Which OAuth providers are enabled for this backend. */
|
|
86
|
+
getProviders(): Promise<Record<string, ProviderInfo>>;
|
|
87
|
+
sendMagicLink(opts: { email: string; redirectTo?: string }): Promise<Result<{ sent: boolean }>>;
|
|
88
|
+
verifyMagicLink(token: string): Promise<Result<Session>>;
|
|
89
|
+
sendOtp(opts: { email: string }): Promise<Result<{ sent: boolean }>>;
|
|
90
|
+
verifyOtp(opts: { email: string; code: string }): Promise<Result<Session>>;
|
|
91
|
+
getUser(): User | null;
|
|
92
|
+
getToken(): string | null;
|
|
93
|
+
/** OAuth error code from the last redirect, if any. */
|
|
94
|
+
lastError(): string | null;
|
|
95
|
+
signOut(): Promise<{ error: null }>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface StorageBucket {
|
|
99
|
+
upload(path: string, file: Blob | File | ArrayBuffer | string, opts?: { contentType?: string }): Promise<Result<{ bucket: string; path: string; bytes: number; url: string }>>;
|
|
100
|
+
download(path: string): Promise<Result<Blob>>;
|
|
101
|
+
getPublicUrl(path: string): string;
|
|
102
|
+
remove(path: string): Promise<Result<{ removed: boolean }>>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface StorageApi {
|
|
106
|
+
from(bucket: string): StorageBucket;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface FunctionsApi {
|
|
110
|
+
invoke<T = any>(slug: string, body?: any): Promise<Result<T>>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface VectorApi {
|
|
114
|
+
search<T = any>(query: {
|
|
115
|
+
table: string;
|
|
116
|
+
column: string;
|
|
117
|
+
embedding: number[];
|
|
118
|
+
limit?: number;
|
|
119
|
+
metric?: "cosine" | "l2" | "ip";
|
|
120
|
+
}): Promise<Result<T[]>>;
|
|
121
|
+
embed(input: string | string[]): Promise<Result<{ embedding: number[]; embeddings: number[][]; model: string; dimensions: number }>>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface PushApi {
|
|
125
|
+
isSupported(): boolean;
|
|
126
|
+
/** Registers the service worker + subscribes via PushManager, then stores the subscription. */
|
|
127
|
+
subscribe(opts?: { serviceWorker?: string }): Promise<Result<any>>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface LingCodeClient {
|
|
131
|
+
readonly url: string;
|
|
132
|
+
readonly anonKey: string;
|
|
133
|
+
readonly backendId: string;
|
|
134
|
+
/** Resolves after any auth redirect in the URL is consumed. */
|
|
135
|
+
readonly ready: Promise<LingCodeClient>;
|
|
136
|
+
from<T = any>(table: string): QueryBuilder<T>;
|
|
137
|
+
auth: AuthApi;
|
|
138
|
+
storage: StorageApi;
|
|
139
|
+
functions: FunctionsApi;
|
|
140
|
+
vector: VectorApi;
|
|
141
|
+
push: PushApi;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface CreateClientOptions {
|
|
145
|
+
/** Set false to skip reading ?lc_session/?lc_magic from the URL on construct. */
|
|
146
|
+
detectSessionInUrl?: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createClient(url: string, anonKey: string, options?: CreateClientOptions): LingCodeClient;
|
|
150
|
+
export const version: string;
|
|
151
|
+
|
|
152
|
+
declare const LingCode: {
|
|
153
|
+
createClient: typeof createClient;
|
|
154
|
+
version: string;
|
|
155
|
+
};
|
|
156
|
+
export default LingCode;
|
|
157
|
+
|
|
158
|
+
declare global {
|
|
159
|
+
interface Window {
|
|
160
|
+
LingCode: typeof LingCode;
|
|
161
|
+
/** Pre-injected in LingCode /try preview & published apps. */
|
|
162
|
+
lingcode?: LingCodeClient;
|
|
163
|
+
LINGCODE_BACKEND_URL?: string;
|
|
164
|
+
LINGCODE_BACKEND_ANON_KEY?: string;
|
|
165
|
+
}
|
|
166
|
+
}
|
package/index.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// ESM entry for lingcode-js. The implementation lives in the UMD file
|
|
2
|
+
// lingcode-v1.js (one source of truth, also served at lingcode.dev/sdk and used
|
|
3
|
+
// as the browser <script> build). Node ESM + bundlers interop the CJS default
|
|
4
|
+
// export; this shim re-exports it as named + default ESM bindings.
|
|
5
|
+
import LingCode from './lingcode-v1.js';
|
|
6
|
+
|
|
7
|
+
export const createClient = LingCode.createClient;
|
|
8
|
+
export const version = LingCode.version;
|
|
9
|
+
export default LingCode;
|
package/lingcode-sw.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* lingcode-sw.js — service worker for LingCode Web Push.
|
|
3
|
+
*
|
|
4
|
+
* Registered by the SDK's client.push.subscribe(). Renders incoming push
|
|
5
|
+
* payloads as notifications and focuses/opens the app on click. The push SEND
|
|
6
|
+
* path (per-backend VAPID keys, /push/subscribe + /push/send routes) lands with
|
|
7
|
+
* the notifications update — this worker is the stable client half, shipped with
|
|
8
|
+
* the SDK so the subscribe() flow has something to register.
|
|
9
|
+
*
|
|
10
|
+
* Note on scope: a service worker only controls pages under the path it's served
|
|
11
|
+
* from. Apps served on their own origin should host this file at their origin
|
|
12
|
+
* root (or pass { serviceWorker } to push.subscribe) so it can claim the app.
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
self.addEventListener('install', function () { self.skipWaiting(); });
|
|
17
|
+
self.addEventListener('activate', function (event) { event.waitUntil(self.clients.claim()); });
|
|
18
|
+
|
|
19
|
+
self.addEventListener('push', function (event) {
|
|
20
|
+
var payload = {};
|
|
21
|
+
try { payload = event.data ? event.data.json() : {}; } catch (_) {
|
|
22
|
+
try { payload = { body: event.data ? event.data.text() : '' }; } catch (__) { payload = {}; }
|
|
23
|
+
}
|
|
24
|
+
var title = payload.title || 'Notification';
|
|
25
|
+
var options = {
|
|
26
|
+
body: payload.body || '',
|
|
27
|
+
icon: payload.icon,
|
|
28
|
+
badge: payload.badge,
|
|
29
|
+
data: { url: payload.url || '/' },
|
|
30
|
+
tag: payload.tag,
|
|
31
|
+
};
|
|
32
|
+
event.waitUntil(self.registration.showNotification(title, options));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
self.addEventListener('notificationclick', function (event) {
|
|
36
|
+
event.notification.close();
|
|
37
|
+
var target = (event.notification.data && event.notification.data.url) || '/';
|
|
38
|
+
event.waitUntil(
|
|
39
|
+
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) {
|
|
40
|
+
for (var i = 0; i < list.length; i++) {
|
|
41
|
+
if (list[i].url === target && 'focus' in list[i]) return list[i].focus();
|
|
42
|
+
}
|
|
43
|
+
if (self.clients.openWindow) return self.clients.openWindow(target);
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
});
|
package/lingcode-v1.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* lingcode-js v1 — the official client SDK for a LingCode managed backend.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies, no build step. Wraps the gateway REST endpoints
|
|
5
|
+
* (/api/cloud/be/<id>/*) in a Supabase/Firebase-shaped client so apps write
|
|
6
|
+
* client.from('todos').eq('done', false).select()
|
|
7
|
+
* instead of hand-rolled fetch().
|
|
8
|
+
*
|
|
9
|
+
* Loaded into generated/published apps via a <script> tag; the preview also
|
|
10
|
+
* pre-injects `window.lingcode` already wired to the app's backend. Keeping the
|
|
11
|
+
* two globals (window.LINGCODE_BACKEND_URL / _ANON_KEY) means raw-fetch apps
|
|
12
|
+
* keep working — the SDK is purely additive.
|
|
13
|
+
*
|
|
14
|
+
* Versioned filename (lingcode-v1.js): bump to -v2 only on a BREAKING change so
|
|
15
|
+
* the 4h CDN edge cache never strands an app on an incompatible build.
|
|
16
|
+
*/
|
|
17
|
+
(function (global, factory) {
|
|
18
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
19
|
+
else global.LingCode = factory();
|
|
20
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
// ── small helpers ────────────────────────────────────────────────────
|
|
24
|
+
function trimSlash(s) { return String(s || '').replace(/\/+$/, ''); }
|
|
25
|
+
function lastSegment(u) { var p = trimSlash(u).split('/'); return p[p.length - 1] || ''; }
|
|
26
|
+
|
|
27
|
+
function makeError(message, code, status) {
|
|
28
|
+
var e = new Error(message || 'Request failed');
|
|
29
|
+
e.code = code || null; e.status = status || 0; e.name = 'LingCodeError';
|
|
30
|
+
return e;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Base64-decode a JWT payload (no verification — purely to read sub/email
|
|
34
|
+
// for getUser() after an OAuth round-trip where we only receive the token).
|
|
35
|
+
function decodeJwt(token) {
|
|
36
|
+
try {
|
|
37
|
+
var part = String(token).split('.')[1];
|
|
38
|
+
if (!part) return null;
|
|
39
|
+
var b64 = part.replace(/-/g, '+').replace(/_/g, '/');
|
|
40
|
+
while (b64.length % 4) b64 += '=';
|
|
41
|
+
return JSON.parse(decodeURIComponent(escape(atob(b64))));
|
|
42
|
+
} catch (_) { return null; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Turn a File/Blob/ArrayBuffer/string into base64 for storage uploads.
|
|
46
|
+
function toBase64(input) {
|
|
47
|
+
function bytesToB64(bytes) {
|
|
48
|
+
var binary = '', chunk = 0x8000;
|
|
49
|
+
for (var i = 0; i < bytes.length; i += chunk) {
|
|
50
|
+
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
|
51
|
+
}
|
|
52
|
+
return btoa(binary);
|
|
53
|
+
}
|
|
54
|
+
if (typeof input === 'string') return Promise.resolve(btoa(unescape(encodeURIComponent(input))));
|
|
55
|
+
if (input instanceof ArrayBuffer) return Promise.resolve(bytesToB64(new Uint8Array(input)));
|
|
56
|
+
if (input && typeof input.arrayBuffer === 'function') {
|
|
57
|
+
return input.arrayBuffer().then(function (buf) { return bytesToB64(new Uint8Array(buf)); });
|
|
58
|
+
}
|
|
59
|
+
return Promise.reject(makeError('upload() needs a File, Blob, ArrayBuffer or string', 'invalid_input'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function lsGet(key) { try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch (_) { return null; } }
|
|
63
|
+
function lsSet(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch (_) {} }
|
|
64
|
+
function lsDel(key) { try { localStorage.removeItem(key); } catch (_) {} }
|
|
65
|
+
|
|
66
|
+
// ── the client ───────────────────────────────────────────────────────
|
|
67
|
+
function createClient(url, anonKey, options) {
|
|
68
|
+
return new LingCodeClient(url, anonKey, options || {});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function LingCodeClient(url, anonKey, options) {
|
|
72
|
+
this.url = trimSlash(url);
|
|
73
|
+
this.anonKey = String(anonKey || '');
|
|
74
|
+
this.backendId = lastSegment(this.url);
|
|
75
|
+
this._sessionKey = 'lingcode.session.' + this.backendId;
|
|
76
|
+
this._session = lsGet(this._sessionKey); // { user, token } | null
|
|
77
|
+
|
|
78
|
+
this.auth = new AuthApi(this);
|
|
79
|
+
this.storage = new StorageNamespace(this);
|
|
80
|
+
this.functions = new FunctionsApi(this);
|
|
81
|
+
this.vector = new VectorApi(this);
|
|
82
|
+
this.push = new PushApi(this);
|
|
83
|
+
|
|
84
|
+
// Finalize any OAuth / magic-link redirect sitting in the URL. `ready`
|
|
85
|
+
// resolves once that's done so apps can `await client.ready` before reading
|
|
86
|
+
// getUser(). Sync sources (lc_session) resolve immediately.
|
|
87
|
+
this.ready = (options.detectSessionInUrl === false)
|
|
88
|
+
? Promise.resolve(this)
|
|
89
|
+
: this._consumeUrlSession();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The bearer to send: the signed-in user's JWT when present, else the anon key.
|
|
93
|
+
LingCodeClient.prototype._token = function () {
|
|
94
|
+
return (this._session && this._session.token) || this.anonKey;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
LingCodeClient.prototype._setSession = function (data) {
|
|
98
|
+
if (!data || !data.token) return data;
|
|
99
|
+
this._session = { user: data.user || decodeUserFromToken(data.token), token: data.token };
|
|
100
|
+
lsSet(this._sessionKey, this._session);
|
|
101
|
+
return data;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Core POST → unwrap { ok, data } → return data, or throw a normalized error.
|
|
105
|
+
LingCodeClient.prototype._req = function (op, body) {
|
|
106
|
+
var self = this;
|
|
107
|
+
return fetch(self.url + '/' + op, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + self._token() },
|
|
110
|
+
body: JSON.stringify(body || {}),
|
|
111
|
+
}).then(function (res) {
|
|
112
|
+
return res.json().catch(function () { return null; }).then(function (json) {
|
|
113
|
+
if (!res.ok || !json || json.ok === false) {
|
|
114
|
+
throw makeError((json && (json.message || json.error)) || ('HTTP ' + res.status),
|
|
115
|
+
json && json.error, res.status);
|
|
116
|
+
}
|
|
117
|
+
return json.data;
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
LingCodeClient.prototype._get = function (path) {
|
|
123
|
+
var self = this;
|
|
124
|
+
return fetch(self.url + path, { headers: { authorization: 'Bearer ' + self._token() } })
|
|
125
|
+
.then(function (res) { return res.json().catch(function () { return null; }).then(function (j) { return { res: res, json: j }; }); });
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
LingCodeClient.prototype.from = function (table) { return new Query(this, table); };
|
|
129
|
+
|
|
130
|
+
// Read the URL for a session handed back by an auth redirect, finalize it,
|
|
131
|
+
// and strip the params so a refresh doesn't re-trigger.
|
|
132
|
+
LingCodeClient.prototype._consumeUrlSession = function () {
|
|
133
|
+
var self = this;
|
|
134
|
+
if (typeof location === 'undefined' || typeof URLSearchParams === 'undefined') return Promise.resolve(self);
|
|
135
|
+
var qs = new URLSearchParams(location.search);
|
|
136
|
+
var done = function () {
|
|
137
|
+
['lc_session', 'lc_magic', 'lc_error'].forEach(function (k) { qs.delete(k); });
|
|
138
|
+
try {
|
|
139
|
+
var rest = qs.toString();
|
|
140
|
+
var clean = location.pathname + (rest ? '?' + rest : '') + location.hash;
|
|
141
|
+
history.replaceState(null, '', clean);
|
|
142
|
+
} catch (_) {}
|
|
143
|
+
return self;
|
|
144
|
+
};
|
|
145
|
+
var sess = qs.get('lc_session');
|
|
146
|
+
if (sess) { self._setSession({ token: sess }); return Promise.resolve(done()); }
|
|
147
|
+
var magic = qs.get('lc_magic');
|
|
148
|
+
if (magic) {
|
|
149
|
+
return self._req('auth/magiclink/verify', { token: magic })
|
|
150
|
+
.then(function (d) { self._setSession(d); }).catch(function () {})
|
|
151
|
+
.then(done);
|
|
152
|
+
}
|
|
153
|
+
// lc_error: leave it for the app to read via client.auth.lastError(), strip later.
|
|
154
|
+
if (qs.get('lc_error')) { self._authError = qs.get('lc_error'); return Promise.resolve(done()); }
|
|
155
|
+
return Promise.resolve(self);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function decodeUserFromToken(token) {
|
|
159
|
+
var c = decodeJwt(token);
|
|
160
|
+
if (!c) return null;
|
|
161
|
+
return { id: c.sub || null, email: c.email || null };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── query builder: filters first, terminal op last ───────────────────
|
|
165
|
+
// client.from('t').eq('done', false).order('created_at',{ascending:false}).limit(50).select()
|
|
166
|
+
// client.from('t').eq('id', 1).update({ done: true }) // where is required
|
|
167
|
+
// client.from('t').eq('id', 1).delete()
|
|
168
|
+
// client.from('t').insert({ ... }) // or insert([ ...rows ])
|
|
169
|
+
function Query(client, table) {
|
|
170
|
+
this.c = client; this.table = table;
|
|
171
|
+
this._where = {}; this._order = null; this._limit = null; this._offset = null;
|
|
172
|
+
}
|
|
173
|
+
function relOp(name) {
|
|
174
|
+
return function (col, val) { this._where[col] = makeRel(this._where[col], name, val); return this; };
|
|
175
|
+
}
|
|
176
|
+
function makeRel(existing, op, val) {
|
|
177
|
+
var node = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
|
|
178
|
+
node[op] = val; return node;
|
|
179
|
+
}
|
|
180
|
+
Query.prototype.eq = function (col, val) { this._where[col] = val; return this; };
|
|
181
|
+
Query.prototype.neq = relOp('neq');
|
|
182
|
+
Query.prototype.gt = relOp('gt');
|
|
183
|
+
Query.prototype.gte = relOp('gte');
|
|
184
|
+
Query.prototype.lt = relOp('lt');
|
|
185
|
+
Query.prototype.lte = relOp('lte');
|
|
186
|
+
Query.prototype.like = relOp('like');
|
|
187
|
+
Query.prototype.ilike = relOp('ilike');
|
|
188
|
+
Query.prototype.in = function (col, arr) { this._where[col] = { in: arr }; return this; };
|
|
189
|
+
// .is(col, null) → IS NULL ; .is(col, 'not_null') → IS NOT NULL
|
|
190
|
+
Query.prototype.is = function (col, val) { this._where[col] = (val === null) ? null : { is: val }; return this; };
|
|
191
|
+
// Merge a plain object of equality filters (Supabase-style .match()).
|
|
192
|
+
Query.prototype.match = function (obj) {
|
|
193
|
+
for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) this._where[k] = obj[k];
|
|
194
|
+
return this;
|
|
195
|
+
};
|
|
196
|
+
Query.prototype.order = function (col, opts) {
|
|
197
|
+
this._order = { column: col, ascending: opts ? opts.ascending !== false : true }; return this;
|
|
198
|
+
};
|
|
199
|
+
Query.prototype.limit = function (n) { this._limit = n; return this; };
|
|
200
|
+
Query.prototype.range = function (from, to) { this._offset = from; this._limit = (to - from + 1); return this; };
|
|
201
|
+
|
|
202
|
+
function settle(promise) {
|
|
203
|
+
return promise.then(function (data) { return { data: data, error: null }; },
|
|
204
|
+
function (error) { return { data: null, error: error }; });
|
|
205
|
+
}
|
|
206
|
+
function hasWhere(w) { for (var k in w) if (Object.prototype.hasOwnProperty.call(w, k)) return true; return false; }
|
|
207
|
+
|
|
208
|
+
Query.prototype.select = function () {
|
|
209
|
+
return settle(this.c._req('select', {
|
|
210
|
+
table: this.table, where: this._where, order: this._order,
|
|
211
|
+
limit: this._limit, offset: this._offset,
|
|
212
|
+
}));
|
|
213
|
+
};
|
|
214
|
+
Query.prototype.insert = function (rowOrRows) {
|
|
215
|
+
return settle(this.c._req('insert', { table: this.table, row: rowOrRows }));
|
|
216
|
+
};
|
|
217
|
+
Query.prototype.update = function (patch) {
|
|
218
|
+
if (!hasWhere(this._where)) return Promise.resolve({ data: null, error: makeError('update() requires a filter (.eq/.match) — refusing an unscoped update', 'where_required') });
|
|
219
|
+
return settle(this.c._req('update', { table: this.table, where: this._where, patch: patch }));
|
|
220
|
+
};
|
|
221
|
+
Query.prototype.delete = function () {
|
|
222
|
+
if (!hasWhere(this._where)) return Promise.resolve({ data: null, error: makeError('delete() requires a filter (.eq/.match) — refusing an unscoped delete', 'where_required') });
|
|
223
|
+
return settle(this.c._req('delete', { table: this.table, where: this._where }));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Live updates: client.from('todos').subscribe(cb) → returns an unsubscribe fn.
|
|
227
|
+
// Each cb receives { table, type: 'INSERT'|'UPDATE'|'DELETE', row }. RLS is
|
|
228
|
+
// enforced server-side, so a signed-in user only ever sees their own rows.
|
|
229
|
+
Query.prototype.subscribe = function (cb, onError) {
|
|
230
|
+
var es = new EventSource(this.c.url + '/realtime?table=' + encodeURIComponent(this.table)
|
|
231
|
+
+ '&apikey=' + encodeURIComponent(this.c._token()));
|
|
232
|
+
es.addEventListener('change', function (e) {
|
|
233
|
+
try { cb(JSON.parse(e.data)); } catch (_) {}
|
|
234
|
+
});
|
|
235
|
+
if (onError) es.onerror = onError; // EventSource auto-reconnects
|
|
236
|
+
return function unsubscribe() { try { es.close(); } catch (_) {} };
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// ── auth ─────────────────────────────────────────────────────────────
|
|
240
|
+
function AuthApi(client) { this.c = client; }
|
|
241
|
+
AuthApi.prototype.signUp = function (creds) { return settle(this._post('auth/signup', creds)); };
|
|
242
|
+
AuthApi.prototype.signIn = AuthApi.prototype.signInWithPassword = function (creds) { return settle(this._post('auth/signin', creds)); };
|
|
243
|
+
AuthApi.prototype._post = function (op, body) {
|
|
244
|
+
var self = this;
|
|
245
|
+
return this.c._req(op, body).then(function (d) { self.c._setSession(d); return d; });
|
|
246
|
+
};
|
|
247
|
+
// Social login is a TOP-LEVEL navigation (not fetch); on return the SDK reads
|
|
248
|
+
// ?lc_session= and stores it automatically (see _consumeUrlSession).
|
|
249
|
+
AuthApi.prototype.signInWithOAuth = function (provider, opts) {
|
|
250
|
+
var redirect = (opts && opts.redirectTo) || location.href;
|
|
251
|
+
location.href = this.c.url + '/auth/oauth/' + encodeURIComponent(provider)
|
|
252
|
+
+ '/start?redirect_url=' + encodeURIComponent(redirect);
|
|
253
|
+
};
|
|
254
|
+
// Which OAuth buttons to render — only show providers whose .available is true.
|
|
255
|
+
AuthApi.prototype.getProviders = function () {
|
|
256
|
+
return this.c._get('/auth/providers').then(function (r) {
|
|
257
|
+
if (!r.res.ok || !r.json) throw makeError('providers probe failed', null, r.res.status);
|
|
258
|
+
return r.json.providers || {};
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
AuthApi.prototype.sendMagicLink = function (opts) {
|
|
262
|
+
return settle(this.c._req('auth/magiclink/request', {
|
|
263
|
+
email: opts.email, redirect_url: (opts && opts.redirectTo) || location.href,
|
|
264
|
+
}));
|
|
265
|
+
};
|
|
266
|
+
AuthApi.prototype.verifyMagicLink = function (token) { return settle(this._post('auth/magiclink/verify', { token: token })); };
|
|
267
|
+
AuthApi.prototype.sendOtp = function (opts) { return settle(this.c._req('auth/otp/request', { email: opts.email })); };
|
|
268
|
+
AuthApi.prototype.verifyOtp = function (opts) { return settle(this._post('auth/otp/verify', { email: opts.email, code: opts.code })); };
|
|
269
|
+
AuthApi.prototype.getUser = function () { return (this.c._session && this.c._session.user) || null; };
|
|
270
|
+
AuthApi.prototype.getToken = function () { return (this.c._session && this.c._session.token) || null; };
|
|
271
|
+
AuthApi.prototype.lastError = function () { return this.c._authError || null; };
|
|
272
|
+
AuthApi.prototype.signOut = function () { this.c._session = null; lsDel(this.c._sessionKey); return Promise.resolve({ error: null }); };
|
|
273
|
+
|
|
274
|
+
// ── storage ──────────────────────────────────────────────────────────
|
|
275
|
+
function StorageNamespace(client) { this.c = client; }
|
|
276
|
+
StorageNamespace.prototype.from = function (bucket) { return new Bucket(this.c, bucket || 'public'); };
|
|
277
|
+
|
|
278
|
+
function Bucket(client, bucket) { this.c = client; this.bucket = bucket; }
|
|
279
|
+
Bucket.prototype.upload = function (path, file, opts) {
|
|
280
|
+
var self = this;
|
|
281
|
+
var contentType = (opts && opts.contentType) || (file && file.type) || 'application/octet-stream';
|
|
282
|
+
return settle(toBase64(file).then(function (data_b64) {
|
|
283
|
+
return self.c._req('storage/upload', { bucket: self.bucket, path: path, content_type: contentType, data_b64: data_b64 });
|
|
284
|
+
}));
|
|
285
|
+
};
|
|
286
|
+
Bucket.prototype.getPublicUrl = function (path) {
|
|
287
|
+
return this.c.url + '/storage/object?bucket=' + encodeURIComponent(this.bucket) + '&path=' + encodeURIComponent(path);
|
|
288
|
+
};
|
|
289
|
+
Bucket.prototype.download = function (path) {
|
|
290
|
+
return fetch(this.getPublicUrl(path)).then(function (res) {
|
|
291
|
+
if (!res.ok) return { data: null, error: makeError('object not found', 'object_not_found', res.status) };
|
|
292
|
+
return res.blob().then(function (blob) { return { data: blob, error: null }; });
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
// remove() lands with the S3/Spaces storage update; calls the route now so the
|
|
296
|
+
// surface is stable (returns { error } until that route ships).
|
|
297
|
+
Bucket.prototype.remove = function (path) {
|
|
298
|
+
return settle(this.c._req('storage/remove', { bucket: this.bucket, path: path }));
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// ── functions ────────────────────────────────────────────────────────
|
|
302
|
+
function FunctionsApi(client) { this.c = client; }
|
|
303
|
+
// The gateway reads req.body.input, so wrap the caller's payload.
|
|
304
|
+
FunctionsApi.prototype.invoke = function (slug, body) { return settle(this.c._req('functions/' + slug, { input: body })); };
|
|
305
|
+
|
|
306
|
+
// ── vector / semantic search ─────────────────────────────────────────
|
|
307
|
+
function VectorApi(client) { this.c = client; }
|
|
308
|
+
VectorApi.prototype.search = function (q) {
|
|
309
|
+
return settle(this.c._req('vector/search', {
|
|
310
|
+
table: q.table, column: q.column, embedding: q.embedding, limit: q.limit, metric: q.metric,
|
|
311
|
+
}));
|
|
312
|
+
};
|
|
313
|
+
VectorApi.prototype.embed = function (input) { return settle(this.c._req('vector/embed', { input: input })); };
|
|
314
|
+
|
|
315
|
+
// ── push (Web Push) — server lands with the notifications update ──────
|
|
316
|
+
function PushApi(client) { this.c = client; }
|
|
317
|
+
PushApi.prototype.isSupported = function () {
|
|
318
|
+
return typeof navigator !== 'undefined' && 'serviceWorker' in navigator
|
|
319
|
+
&& typeof window !== 'undefined' && 'PushManager' in window;
|
|
320
|
+
};
|
|
321
|
+
PushApi.prototype.subscribe = function (opts) {
|
|
322
|
+
var self = this;
|
|
323
|
+
if (!this.isSupported()) return Promise.resolve({ data: null, error: makeError('push not supported in this browser', 'unsupported') });
|
|
324
|
+
var swUrl = (opts && opts.serviceWorker) || 'https://lingcode.dev/sdk/lingcode-sw.js';
|
|
325
|
+
return navigator.serviceWorker.register(swUrl).then(function (reg) {
|
|
326
|
+
return self.c._get('/push/vapid-public').then(function (r) {
|
|
327
|
+
if (!r.res.ok || !r.json || !r.json.data) throw makeError('push not enabled for this backend', 'push_not_configured', r.res.status);
|
|
328
|
+
return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(r.json.data.key || r.json.data) });
|
|
329
|
+
});
|
|
330
|
+
}).then(function (sub) {
|
|
331
|
+
return self.c._req('push/subscribe', { subscription: sub.toJSON ? sub.toJSON() : sub });
|
|
332
|
+
}).then(function (d) { return { data: d, error: null }; }, function (e) { return { data: null, error: e }; });
|
|
333
|
+
};
|
|
334
|
+
function urlBase64ToUint8Array(base64) {
|
|
335
|
+
var pad = '='.repeat((4 - base64.length % 4) % 4);
|
|
336
|
+
var b64 = (base64 + pad).replace(/-/g, '+').replace(/_/g, '/');
|
|
337
|
+
var raw = atob(b64), out = new Uint8Array(raw.length);
|
|
338
|
+
for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { createClient: createClient, version: '1.0.0' };
|
|
343
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lingcode-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official client SDK for a LingCode managed backend — database, auth, realtime, storage, serverless functions, vector search, and push.",
|
|
5
|
+
"main": "lingcode-v1.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"browser": "lingcode-v1.js",
|
|
8
|
+
"unpkg": "lingcode-v1.js",
|
|
9
|
+
"jsdelivr": "lingcode-v1.js",
|
|
10
|
+
"types": "index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"import": "./index.mjs",
|
|
15
|
+
"require": "./lingcode-v1.js",
|
|
16
|
+
"default": "./lingcode-v1.js"
|
|
17
|
+
},
|
|
18
|
+
"./sw": "./lingcode-sw.js",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lingcode-v1.js",
|
|
23
|
+
"lingcode-sw.js",
|
|
24
|
+
"index.mjs",
|
|
25
|
+
"index.d.ts",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"keywords": [
|
|
30
|
+
"lingcode",
|
|
31
|
+
"backend",
|
|
32
|
+
"baas",
|
|
33
|
+
"database",
|
|
34
|
+
"postgres",
|
|
35
|
+
"auth",
|
|
36
|
+
"realtime",
|
|
37
|
+
"storage",
|
|
38
|
+
"vector-search",
|
|
39
|
+
"push",
|
|
40
|
+
"supabase",
|
|
41
|
+
"firebase"
|
|
42
|
+
],
|
|
43
|
+
"homepage": "https://lingcode.dev",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/Xavierhuang/LingCode.git",
|
|
47
|
+
"directory": "website/sdk"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/Xavierhuang/LingCode/issues"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|