phase 0 & 1 ✅, onto the next logic
This commit is contained in:
@@ -11,6 +11,7 @@ This document defines the laws, principles, and rule sets that govern this codeb
|
|||||||
1. **No testing by yourself** — All testing is done by the team.
|
1. **No testing by yourself** — All testing is done by the team.
|
||||||
2. **No assumptions about code or domain logic** — Always confirm and be sure before making changes.
|
2. **No assumptions about code or domain logic** — Always confirm and be sure before making changes.
|
||||||
3. **No running scripts** — Do not run build, dev, test, or migrate scripts unless explicitly approved.
|
3. **No running scripts** — Do not run build, dev, test, or migrate scripts unless explicitly approved.
|
||||||
|
4. **No touching migration files** — Do not mess with the `migrations` sql dir, as those are generated manually via drizzle orm
|
||||||
|
|
||||||
More rules are only to be added by the human, in case such a suggestion becomes viable.
|
More rules are only to be added by the human, in case such a suggestion becomes viable.
|
||||||
|
|
||||||
|
|||||||
198
README.md
198
README.md
@@ -1,3 +1,199 @@
|
|||||||
# Illusory MAPP
|
# Illusory MAPP
|
||||||
|
|
||||||
An internal automation platform built collaboratively. The premise is simple: aggregate data from each member's personal devices — mobile and otherwise — into a single centralized hub, then execute complex group-based automations on top of that collective data. The end goal is a shared automation center where the group's combined device data drives coordinated, programmable workflows.
|
Internal automation platform for aggregating device data into one backend, then running coordinated automations on top of that data.
|
||||||
|
|
||||||
|
## Current Goal (Server-Side Stage 1)
|
||||||
|
|
||||||
|
Build the server-side ingestion and admin visibility for:
|
||||||
|
|
||||||
|
1. Device registration
|
||||||
|
2. Device heartbeat/ping tracking
|
||||||
|
3. SMS ingestion (ongoing)
|
||||||
|
4. Media metadata ingestion (initial sync)
|
||||||
|
5. Admin UI to inspect devices, SMS, and media
|
||||||
|
|
||||||
|
Mobile app implementation is out of scope for now.
|
||||||
|
|
||||||
|
## Scope and Non-Goals
|
||||||
|
|
||||||
|
### In scope now
|
||||||
|
|
||||||
|
- Processor endpoints that receive mobile data
|
||||||
|
- Domain logic + persistence for devices, SMS, media sync metadata
|
||||||
|
- Admin UI read views with polling for SMS (every 5 seconds)
|
||||||
|
- Observability for ingestion and read flows (logs + traces)
|
||||||
|
|
||||||
|
### Not in scope now
|
||||||
|
|
||||||
|
- Mobile app UX/screens or client implementation
|
||||||
|
- Automation execution workflows
|
||||||
|
- Advanced media processing pipelines
|
||||||
|
|
||||||
|
## Delivery Plan (Actionable Tasks)
|
||||||
|
|
||||||
|
Use this as the step-by-step implementation checklist.
|
||||||
|
|
||||||
|
### Phase 0: Align Critical Decisions (before coding)
|
||||||
|
|
||||||
|
- [x] Confirm device identity strategy (`deviceId` from mobile vs server-generated key).
|
||||||
|
- [x] Confirm admin ownership rule for newly registered devices (single admin user mapping).
|
||||||
|
- [x] Confirm SMS dedup strategy (client message id, hash, or `(deviceId + timestamp + sender + body)`).
|
||||||
|
- [x] Confirm media sync contract: metadata-only now vs metadata + upload references.
|
||||||
|
- [x] Confirm minimal auth for processor endpoints (shared token or signed header) for Stage 1.
|
||||||
|
|
||||||
|
Decisions captured:
|
||||||
|
|
||||||
|
- Keep two identifiers per device:
|
||||||
|
- `id` (server-generated primary key)
|
||||||
|
- `externalDeviceId` (sent by mobile)
|
||||||
|
- All devices are owned by the single admin user in Stage 1.
|
||||||
|
- SMS dedup uses:
|
||||||
|
- optional client message id when available
|
||||||
|
- fallback deterministic hash from `(deviceId + timestamp + sender + body)`.
|
||||||
|
- Media sync is metadata plus upload reference (`fileId`) to the file domain/R2 object.
|
||||||
|
- Stage 1 endpoint auth:
|
||||||
|
- register is open in trusted network
|
||||||
|
- ping/sync endpoints must include device id header; request is allowed only if device exists.
|
||||||
|
|
||||||
|
### Phase 1: Data Model and DB Migrations
|
||||||
|
|
||||||
|
Target: `packages/db/schema/*` + new migration files.
|
||||||
|
|
||||||
|
- [ ] Add `mobile_device` table:
|
||||||
|
- Fields: `id`, `externalDeviceId`, `name`, `manufacturer`, `model`, `androidVersion`, `ownerUserId`, `lastPingAt`, `createdAt`, `updatedAt`.
|
||||||
|
- [ ] Add `mobile_sms` table:
|
||||||
|
- Fields: `id`, `deviceId`, `externalMessageId?`, `sender`, `recipient?`, `body`, `sentAt`, `receivedAt?`, `rawPayload?`, `createdAt`.
|
||||||
|
- [ ] Add `mobile_media_asset` table:
|
||||||
|
- Fields: `id`, `deviceId`, `externalMediaId?`, `mimeType`, `filename?`, `capturedAt?`, `sizeBytes?`, `hash?`, `metadata`, `createdAt`.
|
||||||
|
- [ ] Add indexes for common reads:
|
||||||
|
- `mobile_device.lastPingAt`
|
||||||
|
- `mobile_sms.deviceId + sentAt desc`
|
||||||
|
- `mobile_media_asset.deviceId + createdAt desc`
|
||||||
|
- [ ] Export schema from `packages/db/schema/index.ts`.
|
||||||
|
|
||||||
|
Definition of done:
|
||||||
|
|
||||||
|
- [ ] Tables and indexes exist in schema + migration files.
|
||||||
|
- [ ] Naming matches existing conventions.
|
||||||
|
|
||||||
|
### Phase 2: Logic Domain (`@pkg/logic`)
|
||||||
|
|
||||||
|
Target: `packages/logic/domains/mobile/*` (new domain).
|
||||||
|
|
||||||
|
- [ ] Create `data.ts` with Valibot schemas and inferred types for:
|
||||||
|
- register device payload
|
||||||
|
- ping payload
|
||||||
|
- sms sync payload
|
||||||
|
- media sync payload
|
||||||
|
- admin query filters/pagination
|
||||||
|
- [ ] Create `errors.ts` with domain error constructors using `getError`.
|
||||||
|
- [ ] Create `repository.ts` with ResultAsync operations for:
|
||||||
|
- upsert device
|
||||||
|
- update last ping
|
||||||
|
- bulk insert sms (idempotent)
|
||||||
|
- bulk insert media metadata (idempotent)
|
||||||
|
- list devices
|
||||||
|
- get device detail
|
||||||
|
- list sms by device (paginated, latest first)
|
||||||
|
- list media by device (paginated, latest first)
|
||||||
|
- delete single media asset
|
||||||
|
- delete device (removed all child sms, and media assets, and by extensionn the associated files in r2+db as well)
|
||||||
|
- [ ] Create `controller.ts` as use-case layer.
|
||||||
|
- [ ] Wrap repository/controller operations with `traceResultAsync` and include `fctx`.
|
||||||
|
|
||||||
|
Definition of done:
|
||||||
|
|
||||||
|
- [ ] Processor and main app can consume this domain without direct DB usage.
|
||||||
|
- [ ] No ad-hoc thrown errors in domain flow; Result pattern is preserved.
|
||||||
|
|
||||||
|
### Phase 3: Processor Ingestion API (`apps/processor`)
|
||||||
|
|
||||||
|
Target: `apps/processor/src/domains/mobile/router.ts`.
|
||||||
|
|
||||||
|
- [ ] Replace stub endpoints with full handlers:
|
||||||
|
- `POST /register`
|
||||||
|
- `PUT /ping`
|
||||||
|
- `PUT /sms/sync`
|
||||||
|
- `PUT /media/sync`
|
||||||
|
- [ ] Validate request body using schemas from `@pkg/logic/domains/mobile/data`.
|
||||||
|
- [ ] Build `FlowExecCtx` per request (flow id always; user/session when available).
|
||||||
|
- [ ] Call mobile controller methods; return `{ data, error }` response shape.
|
||||||
|
- [ ] Add basic endpoint protection agreed in Phase 0.
|
||||||
|
- [ ] Add request-level structured logging for success/failure.
|
||||||
|
|
||||||
|
Definition of done:
|
||||||
|
|
||||||
|
- [ ] Endpoints persist data into new tables.
|
||||||
|
- [ ] Endpoint failures return normalized domain errors.
|
||||||
|
|
||||||
|
### Phase 4: Admin UI in `apps/main`
|
||||||
|
|
||||||
|
Target:
|
||||||
|
|
||||||
|
- `apps/main/src/lib/domains/mobile/*` (new)
|
||||||
|
- `apps/main/src/routes/(main)/devices/*` (new)
|
||||||
|
|
||||||
|
- [ ] Add remote functions:
|
||||||
|
- `getDevicesSQ`
|
||||||
|
- `getDeviceDetailSQ`
|
||||||
|
- `getDeviceSmsSQ`
|
||||||
|
- `getDeviceMediaSQ`
|
||||||
|
- [ ] Add VM (`devices.vm.svelte.ts`) that:
|
||||||
|
- fetches devices list
|
||||||
|
- fetches selected device detail
|
||||||
|
- polls SMS every 5 seconds while device view is open
|
||||||
|
- handles loading/error states
|
||||||
|
- [ ] Add UI pages/components:
|
||||||
|
- `/devices` list with device identity + last ping
|
||||||
|
- `/devices/[deviceId]` detail with tabs:
|
||||||
|
- Device info
|
||||||
|
- SMS feed
|
||||||
|
- Media assets list
|
||||||
|
- [ ] Add sidebar/navigation entry for Devices.
|
||||||
|
|
||||||
|
Definition of done:
|
||||||
|
|
||||||
|
- [ ] Admin can browse devices and open each device detail.
|
||||||
|
- [ ] SMS view refreshes every 5 seconds and shows new data.
|
||||||
|
|
||||||
|
### Phase 5: Observability Stage 1
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
|
||||||
|
- `packages/logic/core/observability.ts` (use existing helpers)
|
||||||
|
- Processor/mobile domain handlers and repository/controller paths
|
||||||
|
|
||||||
|
- [ ] Add span names for key flows:
|
||||||
|
- `mobile.register`
|
||||||
|
- `mobile.ping`
|
||||||
|
- `mobile.sms.sync`
|
||||||
|
- `mobile.media.sync`
|
||||||
|
- `mobile.devices.list`
|
||||||
|
- `mobile.device.detail`
|
||||||
|
- [ ] Add structured domain events with device id, counts, durations.
|
||||||
|
- [ ] Ensure errors include `flowId` consistently.
|
||||||
|
|
||||||
|
Definition of done:
|
||||||
|
|
||||||
|
- [ ] Can trace one request from processor endpoint to DB operation via shared `flowId`.
|
||||||
|
|
||||||
|
### Phase 6: Handoff Readiness (Team Test Phase)
|
||||||
|
|
||||||
|
- [ ] Prepare endpoint payload examples for mobile team.
|
||||||
|
- [ ] Document pagination/sorting contract for admin UI queries.
|
||||||
|
- [ ] Document dedup behavior for SMS/media ingestion.
|
||||||
|
- [ ] Provide a short operator checklist:
|
||||||
|
- register device
|
||||||
|
- verify ping updates
|
||||||
|
- verify sms appears in admin
|
||||||
|
- verify media appears in admin
|
||||||
|
|
||||||
|
## Suggested Build Order
|
||||||
|
|
||||||
|
1. Phase 0
|
||||||
|
2. Phase 1
|
||||||
|
3. Phase 2
|
||||||
|
4. Phase 3
|
||||||
|
5. Phase 4
|
||||||
|
6. Phase 5
|
||||||
|
7. Phase 6
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
|
|
||||||
const mobileRouter = new Hono().get("/", async (c) => {
|
export const mobileRouter = new Hono()
|
||||||
return c.json({ message: "" });
|
.put("/ping", async (c) => {
|
||||||
|
return c.json({ data: "" });
|
||||||
|
})
|
||||||
|
.put("/sms/sync", async (c) => {
|
||||||
|
return c.json({ data: "" });
|
||||||
|
})
|
||||||
|
.put("/media/sync", async (c) => {
|
||||||
|
return c.json({ data: "" });
|
||||||
|
})
|
||||||
|
.post("/register", async (c) => {
|
||||||
|
return c.json({ data: "" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
|
import { mobileRouter } from "./domains/mobile/router.js";
|
||||||
import { serve } from "@hono/node-server";
|
import { serve } from "@hono/node-server";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
app.get("/", (c) => {
|
app.get("/health", (c) => {
|
||||||
return c.text("Hello Hono!");
|
return c.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/ping", (c) => {
|
||||||
|
return c.text("pong");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.route("/api/v1/mobile", mobileRouter);
|
||||||
|
|
||||||
serve(
|
serve(
|
||||||
{
|
{
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
|
|||||||
56
packages/db/migrations/0002_massive_captain_britain.sql
Normal file
56
packages/db/migrations/0002_massive_captain_britain.sql
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
CREATE TABLE "mobile_device" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"external_device_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"manufacturer" text NOT NULL,
|
||||||
|
"model" text NOT NULL,
|
||||||
|
"android_version" text NOT NULL,
|
||||||
|
"owner_user_id" text NOT NULL,
|
||||||
|
"last_ping_at" timestamp,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"updated_at" timestamp NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "mobile_media_asset" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"device_id" integer NOT NULL,
|
||||||
|
"external_media_id" text,
|
||||||
|
"file_id" text NOT NULL,
|
||||||
|
"mime_type" text NOT NULL,
|
||||||
|
"filename" text,
|
||||||
|
"captured_at" timestamp,
|
||||||
|
"size_bytes" integer,
|
||||||
|
"hash" text,
|
||||||
|
"metadata" json,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"updated_at" timestamp NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "mobile_sms" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"device_id" integer NOT NULL,
|
||||||
|
"external_message_id" text,
|
||||||
|
"sender" text NOT NULL,
|
||||||
|
"recipient" text,
|
||||||
|
"body" text NOT NULL,
|
||||||
|
"sent_at" timestamp NOT NULL,
|
||||||
|
"received_at" timestamp,
|
||||||
|
"dedup_hash" text NOT NULL,
|
||||||
|
"raw_payload" json,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"updated_at" timestamp NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "mobile_device" ADD CONSTRAINT "mobile_device_owner_user_id_user_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mobile_media_asset" ADD CONSTRAINT "mobile_media_asset_device_id_mobile_device_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."mobile_device"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mobile_media_asset" ADD CONSTRAINT "mobile_media_asset_file_id_file_id_fk" FOREIGN KEY ("file_id") REFERENCES "public"."file"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mobile_sms" ADD CONSTRAINT "mobile_sms_device_id_mobile_device_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."mobile_device"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "mobile_device_external_device_id_uq" ON "mobile_device" USING btree ("external_device_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "mobile_device_owner_user_id_idx" ON "mobile_device" USING btree ("owner_user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "mobile_device_last_ping_at_idx" ON "mobile_device" USING btree ("last_ping_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "mobile_media_asset_device_created_at_idx" ON "mobile_media_asset" USING btree ("device_id","created_at");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "mobile_media_asset_device_external_media_uq" ON "mobile_media_asset" USING btree ("device_id","external_media_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "mobile_media_asset_file_id_uq" ON "mobile_media_asset" USING btree ("file_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "mobile_sms_device_sent_at_idx" ON "mobile_sms" USING btree ("device_id","sent_at");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "mobile_sms_device_dedup_hash_uq" ON "mobile_sms" USING btree ("device_id","dedup_hash");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "mobile_sms_device_external_msg_uq" ON "mobile_sms" USING btree ("device_id","external_message_id");
|
||||||
1386
packages/db/migrations/meta/0002_snapshot.json
Normal file
1386
packages/db/migrations/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
|||||||
"when": 1772335785371,
|
"when": 1772335785371,
|
||||||
"tag": "0001_silly_venus",
|
"tag": "0001_silly_venus",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1772338633827,
|
||||||
|
"tag": "0002_massive_captain_britain",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
import { user } from "./better.auth.schema";
|
import { user } from "./better.auth.schema";
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
|
|
||||||
// Add this to your existing schema file
|
|
||||||
export const file = pgTable("file", {
|
export const file = pgTable("file", {
|
||||||
id: text("id").primaryKey(), // UUID
|
id: text("id").primaryKey(), // UUID
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./auth.schema";
|
|||||||
export * from "./better.auth.schema";
|
export * from "./better.auth.schema";
|
||||||
export * from "./file.schema";
|
export * from "./file.schema";
|
||||||
export * from "./general.schema";
|
export * from "./general.schema";
|
||||||
|
export * from "./mobile.device.schema";
|
||||||
|
|||||||
145
packages/db/schema/mobile.device.schema.ts
Normal file
145
packages/db/schema/mobile.device.schema.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
json,
|
||||||
|
pgTable,
|
||||||
|
serial,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { user } from "./better.auth.schema";
|
||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import { file } from "./file.schema";
|
||||||
|
|
||||||
|
export const mobileDevice = pgTable(
|
||||||
|
"mobile_device",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
externalDeviceId: text("external_device_id").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
manufacturer: text("manufacturer").notNull(),
|
||||||
|
model: text("model").notNull(),
|
||||||
|
androidVersion: text("android_version").notNull(),
|
||||||
|
ownerUserId: text("owner_user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
lastPingAt: timestamp("last_ping_at"),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
externalDeviceUniqueIdx: uniqueIndex(
|
||||||
|
"mobile_device_external_device_id_uq",
|
||||||
|
).on(table.externalDeviceId),
|
||||||
|
ownerUserIdx: index("mobile_device_owner_user_id_idx").on(
|
||||||
|
table.ownerUserId,
|
||||||
|
),
|
||||||
|
lastPingIdx: index("mobile_device_last_ping_at_idx").on(
|
||||||
|
table.lastPingAt,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mobileSMS = pgTable(
|
||||||
|
"mobile_sms",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
deviceId: integer("device_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => mobileDevice.id, { onDelete: "cascade" }),
|
||||||
|
externalMessageId: text("external_message_id"),
|
||||||
|
sender: text("sender").notNull(),
|
||||||
|
recipient: text("recipient"),
|
||||||
|
body: text("body").notNull(),
|
||||||
|
sentAt: timestamp("sent_at").notNull(),
|
||||||
|
receivedAt: timestamp("received_at"),
|
||||||
|
dedupHash: text("dedup_hash").notNull(),
|
||||||
|
rawPayload: json("raw_payload").$type<Record<string, any>>(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
deviceSentIdx: index("mobile_sms_device_sent_at_idx").on(
|
||||||
|
table.deviceId,
|
||||||
|
table.sentAt,
|
||||||
|
),
|
||||||
|
dedupHashUniqueIdx: uniqueIndex("mobile_sms_device_dedup_hash_uq").on(
|
||||||
|
table.deviceId,
|
||||||
|
table.dedupHash,
|
||||||
|
),
|
||||||
|
externalMessageUniqueIdx: uniqueIndex(
|
||||||
|
"mobile_sms_device_external_msg_uq",
|
||||||
|
).on(table.deviceId, table.externalMessageId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mobileMediaAsset = pgTable(
|
||||||
|
"mobile_media_asset",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
deviceId: integer("device_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => mobileDevice.id, { onDelete: "cascade" }),
|
||||||
|
externalMediaId: text("external_media_id"),
|
||||||
|
fileId: text("file_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => file.id, { onDelete: "cascade" }),
|
||||||
|
mimeType: text("mime_type").notNull(),
|
||||||
|
filename: text("filename"),
|
||||||
|
capturedAt: timestamp("captured_at"),
|
||||||
|
sizeBytes: integer("size_bytes"),
|
||||||
|
hash: text("hash"),
|
||||||
|
metadata: json("metadata").$type<Record<string, any>>(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
deviceCreatedIdx: index("mobile_media_asset_device_created_at_idx").on(
|
||||||
|
table.deviceId,
|
||||||
|
table.createdAt,
|
||||||
|
),
|
||||||
|
externalMediaUniqueIdx: uniqueIndex(
|
||||||
|
"mobile_media_asset_device_external_media_uq",
|
||||||
|
).on(table.deviceId, table.externalMediaId),
|
||||||
|
fileUniqueIdx: uniqueIndex("mobile_media_asset_file_id_uq").on(
|
||||||
|
table.fileId,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mobileDeviceRelations = relations(
|
||||||
|
mobileDevice,
|
||||||
|
({ one, many }) => ({
|
||||||
|
owner: one(user, {
|
||||||
|
fields: [mobileDevice.ownerUserId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
sms: many(mobileSMS),
|
||||||
|
mediaAssets: many(mobileMediaAsset),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mobileSMSRelations = relations(mobileSMS, ({ one }) => ({
|
||||||
|
device: one(mobileDevice, {
|
||||||
|
fields: [mobileSMS.deviceId],
|
||||||
|
references: [mobileDevice.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const mobileMediaAssetRelations = relations(
|
||||||
|
mobileMediaAsset,
|
||||||
|
({ one }) => ({
|
||||||
|
device: one(mobileDevice, {
|
||||||
|
fields: [mobileMediaAsset.deviceId],
|
||||||
|
references: [mobileDevice.id],
|
||||||
|
}),
|
||||||
|
file: one(file, {
|
||||||
|
fields: [mobileMediaAsset.fileId],
|
||||||
|
references: [file.id],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user