@stnd/core 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/CHANGELOG.md +13 -0
- package/package.json +24 -0
- package/src/StandardModel.ts +55 -0
- package/src/StandardRoot.ts +73 -0
- package/src/Visitor.ts +50 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +20 -0
package/CHANGELOG.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stnd/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@stnd/log": "0.2.0",
|
|
12
|
+
"@stnd/utils": "0.2.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"astro": "^5.0.0",
|
|
16
|
+
"@types/node": "^20.0.0"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import { StandardRoot } from "./StandardRoot.js";
|
|
3
|
+
|
|
4
|
+
export abstract class StandardModel<TData, TRoot extends StandardRoot = StandardRoot> {
|
|
5
|
+
public raw: TData;
|
|
6
|
+
protected root: TRoot;
|
|
7
|
+
|
|
8
|
+
constructor(root: TRoot, data: TData) {
|
|
9
|
+
this.root = root;
|
|
10
|
+
this.raw = data;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SERIALIZATION
|
|
15
|
+
* Strips the circular 'root' dependency for Svelte/React
|
|
16
|
+
*/
|
|
17
|
+
toJSON(): TData {
|
|
18
|
+
return this.raw;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* AUTOMATIC CACHING (The "Standard" Way)
|
|
23
|
+
* Replaces manual KV logic
|
|
24
|
+
*/
|
|
25
|
+
protected static async withCache<T>(
|
|
26
|
+
root: StandardRoot,
|
|
27
|
+
key: string,
|
|
28
|
+
fetcher: () => Promise<T | null>,
|
|
29
|
+
ttl: number = 60 // Default 60s
|
|
30
|
+
): Promise<T | null> {
|
|
31
|
+
const { env } = root;
|
|
32
|
+
|
|
33
|
+
// 1. Try KV (Fast Path)
|
|
34
|
+
if (env.GARDEN_KV) {
|
|
35
|
+
try {
|
|
36
|
+
const hit = await env.GARDEN_KV.get(key, { type: "json" });
|
|
37
|
+
if (hit) return hit as T;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.warn("KV Get Error", e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Fetch Source (Slow Path)
|
|
44
|
+
const data = await fetcher();
|
|
45
|
+
|
|
46
|
+
// 3. Update KV (Background Path)
|
|
47
|
+
if (data && env.GARDEN_KV) {
|
|
48
|
+
root.waitUntil(
|
|
49
|
+
env.GARDEN_KV.put(key, JSON.stringify(data), { expirationTtl: ttl })
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Visitor from "./Visitor.js";
|
|
2
|
+
import Log from "@stnd/log";
|
|
3
|
+
|
|
4
|
+
const log = Log({ scope: "Root" });
|
|
5
|
+
|
|
6
|
+
export interface RootServices {
|
|
7
|
+
db: any;
|
|
8
|
+
bucket?: any;
|
|
9
|
+
ai?: any;
|
|
10
|
+
vectorize?: any;
|
|
11
|
+
env?: any;
|
|
12
|
+
ctx?: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Rename to StandardRoot to indicate it's the base
|
|
16
|
+
export class StandardRoot {
|
|
17
|
+
public db: any;
|
|
18
|
+
public env: any;
|
|
19
|
+
public ctx?: any;
|
|
20
|
+
public url: URL;
|
|
21
|
+
|
|
22
|
+
// Visitor is universal (everyone needs auth)
|
|
23
|
+
public visitor!: Visitor;
|
|
24
|
+
|
|
25
|
+
constructor(services: RootServices, url: URL) {
|
|
26
|
+
this.db = services.db;
|
|
27
|
+
this.env = services.env;
|
|
28
|
+
this.ctx = services.ctx;
|
|
29
|
+
this.url = url;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The Factory
|
|
33
|
+
static async enter(context: any): Promise<StandardRoot> {
|
|
34
|
+
const { locals, request } = context;
|
|
35
|
+
let env: any;
|
|
36
|
+
let ctx: any;
|
|
37
|
+
|
|
38
|
+
if ("cfContext" in locals) {
|
|
39
|
+
ctx = (locals as any).cfContext;
|
|
40
|
+
}
|
|
41
|
+
if ("env" in locals) {
|
|
42
|
+
env = (locals as any).env;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback logic
|
|
46
|
+
if (!env) {
|
|
47
|
+
try {
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
const cf = await import("cloudflare:workers");
|
|
50
|
+
env = cf.env;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Env might be missing in some contexts
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const services = { db: env?.DB, env, ctx };
|
|
57
|
+
const root = new this(services, new URL(request.url));
|
|
58
|
+
|
|
59
|
+
// Initialize Visitor (Universal)
|
|
60
|
+
root.visitor = await Visitor.fromUrl(root, context);
|
|
61
|
+
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Helper for background tasks
|
|
66
|
+
public waitUntil(promise: Promise<any>): void {
|
|
67
|
+
if (this.ctx && typeof this.ctx.waitUntil === "function") {
|
|
68
|
+
this.ctx.waitUntil(promise);
|
|
69
|
+
} else {
|
|
70
|
+
promise.catch(err => console.error(err));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/Visitor.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Log from "@stnd/log";
|
|
2
|
+
import type { StandardRoot } from "./StandardRoot.js";
|
|
3
|
+
import type { APIContext } from "astro";
|
|
4
|
+
|
|
5
|
+
const log = Log({ scope: "Visitor" });
|
|
6
|
+
|
|
7
|
+
export interface VisitorData {
|
|
8
|
+
id: number;
|
|
9
|
+
username?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
role?: string;
|
|
12
|
+
tier?: string;
|
|
13
|
+
isAnonymous?: boolean;
|
|
14
|
+
preferences?: any;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default class Visitor {
|
|
19
|
+
public raw: VisitorData;
|
|
20
|
+
protected root?: StandardRoot;
|
|
21
|
+
|
|
22
|
+
constructor(raw: VisitorData, root?: StandardRoot) {
|
|
23
|
+
this.raw = raw;
|
|
24
|
+
this.root = root;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* FACTORY: Create a Visitor within the Root context
|
|
29
|
+
* This is a stub implementation that should be overridden or improved
|
|
30
|
+
* by the specific app implementation if needed, but here we provide
|
|
31
|
+
* the basic "Anonymous" or "Auth" structure.
|
|
32
|
+
*/
|
|
33
|
+
static async fromUrl(root: StandardRoot, context: APIContext): Promise<Visitor> {
|
|
34
|
+
// Basic implementation for now - defaulting to Anonymous
|
|
35
|
+
// The full auth logic involves DB access which we can add later
|
|
36
|
+
// or we can copy the logic from the app.
|
|
37
|
+
// For now, let's keep it simple to get the structure right.
|
|
38
|
+
|
|
39
|
+
const { cookies } = context;
|
|
40
|
+
// Real implementation would check session/jwt here.
|
|
41
|
+
|
|
42
|
+
// Return anonymous visitor
|
|
43
|
+
return new Visitor({
|
|
44
|
+
id: 1,
|
|
45
|
+
username: "anonymous",
|
|
46
|
+
tier: "anonymous",
|
|
47
|
+
isAnonymous: true
|
|
48
|
+
}, root);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"resolveJsonModule": true,
|
|
7
|
+
"isolatedModules": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"src/**/*"
|
|
15
|
+
],
|
|
16
|
+
"exclude": [
|
|
17
|
+
"node_modules",
|
|
18
|
+
"dist"
|
|
19
|
+
]
|
|
20
|
+
}
|