Skip to content

Commit e75fe90

Browse files
committed
2 parents 371445c + a4880db commit e75fe90

19 files changed

Lines changed: 351 additions & 125 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- **`@objectstack/driver-sql` tests failing in CI** — Added `vitest.config.ts` with resolve aliases for `@objectstack/spec/*` subpath exports (`/data`, `/contracts`, `/system`). Without these aliases, vitest could not resolve the source paths at test time, causing all 81 tests to fail with `ERR_MODULE_NOT_FOUND`.
12+
1013
### Added
1114
- **Account portal: full better-auth SDK migration** — All user-settings pages (`/account/*`) and the first-run `/setup` wizard now use typed SDK methods from `@objectstack/client` instead of raw `fetch()` calls against `/api/v1/auth/…`. This fixes several silent bugs (wrong parameter names, invented endpoints) and adds missing features:
1215
- **SDK extensions** (`packages/client`): `auth.{updateUser, changePassword, changeEmail, sendVerificationEmail, verifyEmail, deleteUser, bootstrapStatus}`, `auth.sessions.{list, revoke, revokeOthers, revokeAll}`, `auth.twoFactor.{enable, verifyTotp, disable, generateBackupCodes, verifyBackupCode}`, `auth.accounts.{list, unlink, linkSocial}`, `organizations.{removeMember, updateMemberRole, getActiveMember, leave}`, `organizations.invitations.{list, listMine, cancel, accept, reject, resend}`, `organizations.teams.{list, create, update, delete, listMembers, addMember, removeMember}`.

apps/objectos/README.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,53 @@ Behavior:
172172

173173
### Database (Local / Standalone mode)
174174

175+
ObjectOS in single-project local mode uses **two** databases:
176+
177+
| DB | Purpose | Env var | Default |
178+
|:---|:---|:---|:---|
179+
| **Project DB** | Your business data (records served at `/api/v1/data/*`) | `OS_DATABASE_URL` / `OS_DATABASE_DRIVER` | local SQLite at `.objectstack/data/proj_local.db` |
180+
| **Control DB** | Framework bookkeeping (`sys_organization`, `sys_project`, `sys_user`, …) | `OS_CONTROL_DATABASE_URL` | local SQLite at `.objectstack/data/control.db` |
181+
182+
Other variables:
183+
175184
| Variable | Default | Description |
176185
|:---|:---|:---|
177-
| `OS_DATABASE_URL` | `.objectstack/data/app.db` | SQLite file path, `libsql://`, or `http(s)://` |
178186
| `OS_DATABASE_AUTH_TOKEN` || Auth token for libSQL / Turso |
187+
| `OS_DATABASE_DRIVER` | inferred from URL scheme | Explicit override: `mongodb`, `postgres`, `mysql`, `sqlite`, `turso`, `memory` |
179188

180-
Supported `OS_DATABASE_URL` formats:
189+
The driver is **inferred from the URL scheme** of `OS_DATABASE_URL`
190+
(and `OS_CONTROL_DATABASE_URL`). You almost never need to set
191+
`OS_DATABASE_DRIVER` explicitly.
181192

182-
| Value | Driver |
193+
| `OS_DATABASE_URL` value | Driver |
183194
|:---|:---|
184-
| unset | SQLite — `.objectstack/data/app.db` |
185-
| `file:<path>` | SQLite at that path |
186-
| `libsql://host` | libSQL / Turso |
187-
| `http(s)://host` | libSQL over HTTP (sqld) |
195+
| unset | SQLite — `.objectstack/data/proj_local.db` |
196+
| `file:<path>` / `<path>.db` / `<path>.sqlite` | SQLite at that path |
197+
| `:memory:` | SQLite in-memory |
198+
| `mongodb://…` / `mongodb+srv://…` | **MongoDB** (`@objectstack/driver-mongodb`) |
199+
| `postgres://…` / `postgresql://…` | PostgreSQL (`@objectstack/driver-sql`) |
200+
| `mysql://…` / `mysql2://…` | MySQL (`@objectstack/driver-sql`) |
201+
| `libsql://…` / `https://*.turso.…` | libSQL / Turso (`@objectstack/driver-turso`) |
202+
203+
Examples:
204+
205+
```bash
206+
# Project DB on MongoDB (control DB stays on local SQLite)
207+
OS_DATABASE_URL=mongodb://localhost:27017/objectos pnpm dev
208+
209+
# Project DB on Postgres
210+
OS_DATABASE_URL=postgres://user:pass@host:5432/myapp pnpm dev
211+
212+
# Pin both DBs explicitly (Postgres for project, Turso for control plane)
213+
OS_DATABASE_URL=postgres://user:pass@host/myapp \
214+
OS_CONTROL_DATABASE_URL=libsql://control.turso.io \
215+
OS_DATABASE_AUTH_TOKEN=$TURSO_TOKEN \
216+
pnpm dev
217+
```
218+
219+
For backward compatibility, `OS_DATABASE_URL` is also accepted as a
220+
fallback for the control DB when neither `OS_CONTROL_DATABASE_URL` nor
221+
an explicit programmatic value is set.
188222

189223
### Kernel Tuning
190224

content/docs/concepts/north-star.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,10 @@ preserve Built, reduce Drift, and advance Missing.
498498
- **CLI `publish` routes to project publish endpoint (D2).** `objectstack publish` now POSTs to `/api/v1/cloud/projects/:id/metadata`, accepts `--project` and `--server` flags, and returns `versionId`, `commitId`, and `checksum`[packages/cli/src/commands/publish.ts](packages/cli/src/commands/publish.ts).
499499
- **`apps/objectos` uses ObjectOS runtime mode (M4).** `apps/objectos/objectstack.config.ts` boots via `createBootStack({ runtime: { cloudUrl: ... } })`, separating the ObjectOS runtime from the control plane — [apps/objectos/objectstack.config.ts](apps/objectos/objectstack.config.ts).
500500
- **`apps/cloud` is an independent control plane (D8).** `apps/cloud/objectstack.config.ts` boots via `createBootStack({ mode: 'cloud', ... })`, running the control plane independently without the ObjectOS runtime manifest — [apps/cloud/objectstack.config.ts](apps/cloud/objectstack.config.ts).
501+
- **Storage driver matrix expanded to MongoDB.** `@objectstack/driver-mongodb` ships a full `IDataDriver` (filter translator, aggregation builder, schema sync) with 75 unit tests against `mongodb-memory-server`, and is wired into the CLI / `service-cloud` driver factory — [packages/plugins/driver-mongodb/](packages/plugins/driver-mongodb/), [packages/services/service-cloud/src/driver-factory.ts](packages/services/service-cloud/src/driver-factory.ts).
502+
- **Driver auto-detection from URL scheme.** `OS_DATABASE_URL` / `OS_CONTROL_DATABASE_URL` infer the driver from `mongodb://`, `postgres://`, `mysql://`, `libsql://`, `file:`, `:memory:` so deploys can drop the explicit `OS_DATABASE_DRIVER` flag — [packages/cli/src/commands/serve.ts](packages/cli/src/commands/serve.ts), [apps/objectos/objectstack.config.ts](apps/objectos/objectstack.config.ts).
503+
- **CLI startup banner shows resolved driver + redacted DB URL.** `objectstack serve` prints a `Driver:` line (e.g. `MongoDBDriver → mongodb://admin:****@host/db`) so operators can verify URL inference at a glance — [packages/cli/src/utils/format.ts](packages/cli/src/utils/format.ts).
504+
- **Multi-driver e2e harness.** `examples/app-crm` ships a shared `runDriverAcceptance()` helper plus per-driver Playwright specs (sqlite / mongodb / postgres) and a `scripts/run-driver-acceptance.sh` wrapper that boots `pnpm dev` per-driver — [examples/app-crm/e2e/_acceptance.ts](examples/app-crm/e2e/_acceptance.ts).
501505

502506
### <AlertTriangle className="inline" /> Drift (Needs Cleanup)
503507

content/docs/references/security/rls.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ permissions (CRUD), RLS provides record-level filtering.
2525

2626
- Users only see records from their organization
2727

28-
- `using: "tenant_id = current_user.tenant_id"`
28+
- `using: "organization_id = current_user.organization_id"`
2929

3030
2. **Ownership-Based Access**
3131

@@ -83,7 +83,7 @@ object: 'account',
8383

8484
operation: 'select',
8585

86-
using: 'tenant_id = current_user.tenant_id'
86+
using: 'organization_id = current_user.organization_id'
8787

8888
\}
8989

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { test, expect, request } from '@playwright/test';
2+
3+
/**
4+
* Shared driver-acceptance suite for the CRM example.
5+
* Each driver-specific spec calls `runDriverAcceptance({ label, baseURL })`.
6+
* The CRM dev server is expected to be reachable at `baseURL`,
7+
* already booted with the appropriate `OS_DATABASE_URL` for the driver
8+
* under test (this is wired by the Playwright `webServer` config or by
9+
* a wrapper script that boots the server out-of-band).
10+
*/
11+
export function runDriverAcceptance(opts: { label: string; baseURL?: string }) {
12+
const BASE_URL = opts.baseURL ?? process.env.CRM_BASE_URL ?? 'http://localhost:3001';
13+
14+
test.describe(`CRM running on ${opts.label} driver`, () => {
15+
test('Studio responds', async () => {
16+
const ctx = await request.newContext({ baseURL: BASE_URL });
17+
const studio = await ctx.get('/_studio/');
18+
expect(studio.status()).toBeGreaterThanOrEqual(200);
19+
expect(studio.status()).toBeLessThan(500);
20+
await ctx.dispose();
21+
});
22+
23+
test('seed data is queryable', async () => {
24+
const ctx = await request.newContext({ baseURL: BASE_URL });
25+
const res = await ctx.get('/api/v1/data/account?limit=10');
26+
expect(res.ok()).toBe(true);
27+
const body = await res.json();
28+
expect(body.object).toBe('account');
29+
expect(Array.isArray(body.records)).toBe(true);
30+
expect(body.records.length).toBeGreaterThan(0);
31+
await ctx.dispose();
32+
});
33+
34+
test('CRUD round-trip on account', async () => {
35+
const ctx = await request.newContext({ baseURL: BASE_URL });
36+
const unique = `Playwright ${opts.label} ${Date.now()}`;
37+
38+
const created = await ctx.post('/api/v1/data/account', {
39+
data: { name: unique, industry: 'technology', type: 'prospect' },
40+
});
41+
expect(created.ok()).toBe(true);
42+
const createdBody = await created.json();
43+
const id = createdBody.id ?? createdBody.record?.id;
44+
expect(id).toBeTruthy();
45+
expect(createdBody.record?.name).toBe(unique);
46+
47+
const fetched = await ctx.get(`/api/v1/data/account/${id}`);
48+
expect(fetched.ok()).toBe(true);
49+
const fetchedBody = await fetched.json();
50+
expect(fetchedBody.record?.name).toBe(unique);
51+
52+
const updated = await ctx.patch(`/api/v1/data/account/${id}`, {
53+
data: { industry: 'finance' },
54+
});
55+
expect(updated.ok()).toBe(true);
56+
57+
const reread = await ctx.get(`/api/v1/data/account/${id}`);
58+
const rereadBody = await reread.json();
59+
expect(rereadBody.record?.industry).toBe('finance');
60+
61+
const deleted = await ctx.delete(`/api/v1/data/account/${id}`);
62+
expect(deleted.ok()).toBe(true);
63+
64+
await ctx.dispose();
65+
});
66+
});
67+
}
Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,8 @@
1-
import { test, expect, request } from '@playwright/test';
1+
import { runDriverAcceptance } from './_acceptance';
22

33
/**
4-
* E2E test: verifies the CRM dev server boots with the MongoDB driver
5-
* (selected via `OS_DATABASE_DRIVER=mongodb`) and that CRUD round-trips
6-
* land in the configured MongoDB database.
4+
* Boots `pnpm dev` with `OS_DATABASE_URL=mongodb://...`
5+
* (driver auto-inferred from the URL no OS_DATABASE_DRIVER needed).scheme
6+
* Requires a local mongod listening on the URL.
77
*/
8-
const BASE_URL = process.env.CRM_BASE_URL ?? 'http://localhost:3001';
9-
10-
test.describe('CRM running on MongoDB driver', () => {
11-
test('Studio responds', async () => {
12-
const ctx = await request.newContext({ baseURL: BASE_URL });
13-
const studio = await ctx.get('/_studio/');
14-
expect(studio.status()).toBeGreaterThanOrEqual(200);
15-
expect(studio.status()).toBeLessThan(500);
16-
await ctx.dispose();
17-
});
18-
19-
test('seed data is queryable from MongoDB', async () => {
20-
const ctx = await request.newContext({ baseURL: BASE_URL });
21-
const res = await ctx.get('/api/v1/data/account?limit=10');
22-
expect(res.ok()).toBe(true);
23-
const body = await res.json();
24-
expect(body.object).toBe('account');
25-
expect(Array.isArray(body.records)).toBe(true);
26-
expect(body.records.length).toBeGreaterThan(0);
27-
await ctx.dispose();
28-
});
29-
30-
test('CRUD round-trip on account collection', async () => {
31-
const ctx = await request.newContext({ baseURL: BASE_URL });
32-
const unique = `Playwright Mongo Account ${Date.now()}`;
33-
34-
const created = await ctx.post('/api/v1/data/account', {
35-
data: { name: unique, industry: 'technology', type: 'prospect' },
36-
});
37-
expect(created.ok()).toBe(true);
38-
const createdBody = await created.json();
39-
const id = createdBody.id ?? createdBody.record?.id;
40-
expect(id).toBeTruthy();
41-
expect(createdBody.record?.name).toBe(unique);
42-
43-
const fetched = await ctx.get(`/api/v1/data/account/${id}`);
44-
expect(fetched.ok()).toBe(true);
45-
const fetchedBody = await fetched.json();
46-
expect(fetchedBody.record?.name).toBe(unique);
47-
48-
const updated = await ctx.patch(`/api/v1/data/account/${id}`, {
49-
data: { industry: 'finance' },
50-
});
51-
expect(updated.ok()).toBe(true);
52-
53-
const reread = await ctx.get(`/api/v1/data/account/${id}`);
54-
const rereadBody = await reread.json();
55-
expect(rereadBody.record?.industry).toBe('finance');
56-
57-
const deleted = await ctx.delete(`/api/v1/data/account/${id}`);
58-
expect(deleted.ok()).toBe(true);
59-
60-
await ctx.dispose();
61-
});
62-
});
8+
runDriverAcceptance({ label: 'MongoDB' });
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { runDriverAcceptance } from './_acceptance';
2+
3+
/**
4+
* Boots `pnpm dev` with `OS_DATABASE_URL=postgres://...`
5+
* (driver auto-inferred → SqlDriver(client:'pg')).
6+
* Requires a running PostgreSQL instance reachable at the URL.
7+
*/
8+
runDriverAcceptance({ label: 'PostgreSQL' });
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { runDriverAcceptance } from './_acceptance';
2+
3+
/**
4+
* Boots `pnpm dev` with `OS_DATABASE_URL=file:./<dir>/proj_local.db`
5+
* (driver auto-inferred → SqlDriver(client:'better-sqlite3')).
6+
* Requires no external services — runs anywhere.
7+
*/
8+
runDriverAcceptance({ label: 'SQLite' });

examples/app-crm/objectstack.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import * as cubes from './src/cubes';
1313

1414
// ─── Barrel Imports (one per metadata type) ─────────────────────────
1515
import * as objects from './src/objects';
16-
import * as apis from './src/apis';
1716
import * as actions from './src/actions';
1817
import * as dashboards from './src/dashboards';
1918
import * as reports from './src/reports';
@@ -60,7 +59,6 @@ export default defineStack({
6059

6160
// Auto-collected from barrel index files via Object.values()
6261
objects: Object.values(objects),
63-
apis: Object.values(apis),
6462
actions: Object.values(actions),
6563
dashboards: Object.values(dashboards),
6664
reports: Object.values(reports),

examples/app-crm/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@example/app-crm",
2+
"name": "@objectstack/example-crm",
33
"version": "4.0.4",
44
"description": "Example CRM implementation using ObjectStack Protocol",
55
"license": "Apache-2.0",
@@ -16,7 +16,8 @@
1616
"build": "objectstack build",
1717
"typecheck": "tsc --noEmit",
1818
"test": "objectstack test",
19-
"test:e2e": "playwright test"
19+
"test:e2e": "playwright test",
20+
"test:e2e:drivers": "./scripts/run-driver-acceptance.sh"
2021
},
2122
"dependencies": {
2223
"@objectstack/spec": "workspace:*",

0 commit comments

Comments
 (0)