@open-mercato/core 0.6.8-develop.7079.1.dbf29dd6a1 → 0.6.8-develop.7080.1.9b0d558a3f

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.
Files changed (30) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/devices/api/admin/devices/[id]/route.js +5 -32
  3. package/dist/modules/devices/api/admin/devices/[id]/route.js.map +2 -2
  4. package/dist/modules/devices/api/admin/devices/route.js +14 -1
  5. package/dist/modules/devices/api/admin/devices/route.js.map +2 -2
  6. package/dist/modules/devices/api/deviceList.js +99 -12
  7. package/dist/modules/devices/api/deviceList.js.map +2 -2
  8. package/dist/modules/devices/api/deviceSerialization.js +46 -0
  9. package/dist/modules/devices/api/deviceSerialization.js.map +7 -0
  10. package/dist/modules/devices/api/openapi.js +3 -1
  11. package/dist/modules/devices/api/openapi.js.map +2 -2
  12. package/dist/modules/devices/api/route.js +9 -1
  13. package/dist/modules/devices/api/route.js.map +2 -2
  14. package/dist/modules/devices/backend/devices/[id]/page.js +8 -8
  15. package/dist/modules/devices/backend/devices/[id]/page.js.map +2 -2
  16. package/dist/modules/devices/backend/devices/page.js +12 -12
  17. package/dist/modules/devices/backend/devices/page.js.map +2 -2
  18. package/dist/modules/push_notifications/backend/push_notifications/send/page.js +1 -1
  19. package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +2 -2
  20. package/package.json +7 -7
  21. package/src/modules/devices/AGENTS.md +8 -0
  22. package/src/modules/devices/api/admin/devices/[id]/route.ts +5 -34
  23. package/src/modules/devices/api/admin/devices/route.ts +14 -1
  24. package/src/modules/devices/api/deviceList.ts +135 -11
  25. package/src/modules/devices/api/deviceSerialization.ts +50 -0
  26. package/src/modules/devices/api/openapi.ts +9 -1
  27. package/src/modules/devices/api/route.ts +9 -1
  28. package/src/modules/devices/backend/devices/[id]/page.tsx +15 -15
  29. package/src/modules/devices/backend/devices/page.tsx +20 -20
  30. package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +4 -4
@@ -1,4 +1,4 @@
1
- [build:core] found 4342 entry points
1
+ [build:core] found 4344 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 218 entry points
4
4
  [build:core:generated] built successfully
@@ -13,6 +13,8 @@ import { updateDeviceSchema } from "../../../../data/validators.js";
13
13
  import { isOrganizationReadAccessAllowed } from "@open-mercato/core/modules/directory/utils/organizationScopeGuard";
14
14
  import { resolveDeviceActorUserId } from "../../../auth.js";
15
15
  import { executeUpdate, executeDeactivate } from "../../../deviceOps.js";
16
+ import { serializeDeviceDetail, deviceDetailItemSchema } from "../../../deviceSerialization.js";
17
+ import { DEPRECATED_SNAKE_CASE_NOTICE } from "../../../openapi.js";
16
18
  const logger = createLogger("devices");
17
19
  const metadata = {
18
20
  GET: { requireAuth: true, requireFeatures: ["devices.admin"] },
@@ -24,21 +26,6 @@ async function loadDevice(container, id, tenantId) {
24
26
  const em = container.resolve("em");
25
27
  return findOneWithDecryption(em, UserDevice, { id, tenantId, deletedAt: null }, void 0, { tenantId });
26
28
  }
27
- function serializeDevice(device) {
28
- return {
29
- id: device.id,
30
- user_id: device.userId,
31
- device_id: device.deviceId,
32
- platform: device.platform,
33
- client_app_version: device.clientAppVersion ?? null,
34
- os_version: device.osVersion ?? null,
35
- push_provider: device.pushProvider ?? null,
36
- push_token_updated_at: device.pushTokenUpdatedAt ? device.pushTokenUpdatedAt.toISOString() : null,
37
- last_seen_at: device.lastSeenAt ? device.lastSeenAt.toISOString() : null,
38
- created_at: device.createdAt ? device.createdAt.toISOString() : null,
39
- updated_at: device.updatedAt ? device.updatedAt.toISOString() : null
40
- };
41
- }
42
29
  async function GET(req, { params }) {
43
30
  const { translate } = await resolveTranslations();
44
31
  try {
@@ -60,7 +47,7 @@ async function GET(req, { params }) {
60
47
  if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: device.organizationId ?? null })) {
61
48
  return NextResponse.json({ error: translate("devices.errors.forbidden", "Access denied") }, { status: 403 });
62
49
  }
63
- return NextResponse.json({ item: serializeDevice(device) });
50
+ return NextResponse.json({ item: serializeDeviceDetail(device) });
64
51
  } catch (err) {
65
52
  if (isCrudHttpError(err)) {
66
53
  return NextResponse.json(err.body, { status: err.status });
@@ -154,28 +141,14 @@ async function DELETE(req, { params }) {
154
141
  }
155
142
  const okResponseSchema = z.object({ ok: z.literal(true), id: z.string().uuid().optional() });
156
143
  const errorResponseSchema = z.object({ error: z.string() });
157
- const detailResponseSchema = z.object({
158
- item: z.object({
159
- id: z.string().uuid(),
160
- user_id: z.string().uuid(),
161
- device_id: z.string(),
162
- platform: z.enum(["ios", "android", "web"]),
163
- client_app_version: z.string().nullable(),
164
- os_version: z.string().nullable(),
165
- push_provider: z.string().nullable(),
166
- push_token_updated_at: z.string().nullable(),
167
- last_seen_at: z.string().nullable(),
168
- created_at: z.string().nullable(),
169
- updated_at: z.string().nullable()
170
- })
171
- });
144
+ const detailResponseSchema = z.object({ item: deviceDetailItemSchema });
172
145
  const openApi = {
173
146
  tag: "Devices (admin)",
174
147
  summary: "Admin: read, update or deactivate any device in the tenant",
175
148
  methods: {
176
149
  GET: {
177
150
  summary: "Get any device",
178
- description: "Admin: fetch a single device by id (push_token is never returned).",
151
+ description: `Admin: fetch a single device by id (push_token is never returned). ${DEPRECATED_SNAKE_CASE_NOTICE}`,
179
152
  responses: [{ status: 200, description: "Device", schema: detailResponseSchema }],
180
153
  errors: [
181
154
  { status: 400, description: "Invalid id", schema: errorResponseSchema },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/devices/api/admin/devices/%5Bid%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { UserDevice } from '../../../../data/entities'\nimport { updateDeviceSchema } from '../../../../data/validators'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { resolveDeviceActorUserId } from '../../../auth'\nimport { executeUpdate, executeDeactivate, type DeviceMutationContext } from '../../../deviceOps'\n\nconst logger = createLogger('devices')\n\n// Admin: read/update/deactivate ANY device in the tenant. Gated by devices.admin (no owner check).\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['devices.admin'] },\n PUT: { requireAuth: true, requireFeatures: ['devices.admin'] },\n DELETE: { requireAuth: true, requireFeatures: ['devices.admin'] },\n}\n\nconst paramsSchema = z.object({ id: z.string().uuid() })\n\nasync function loadDevice(\n container: Awaited<ReturnType<typeof createRequestContainer>>,\n id: string,\n tenantId: string,\n): Promise<UserDevice | null> {\n const em = container.resolve('em') as EntityManager\n // push_token is encrypted at rest; the org is not known until the row loads, so pass tenant only \u2014\n // the helper prefers each record's own tenant/org and treats this scope as a fallback.\n return findOneWithDecryption(em, UserDevice, { id, tenantId, deletedAt: null }, undefined, { tenantId })\n}\n\n// push_token is a secret and is never returned.\nfunction serializeDevice(device: UserDevice) {\n return {\n id: device.id,\n user_id: device.userId,\n device_id: device.deviceId,\n platform: device.platform,\n client_app_version: device.clientAppVersion ?? null,\n os_version: device.osVersion ?? null,\n push_provider: device.pushProvider ?? null,\n push_token_updated_at: device.pushTokenUpdatedAt ? device.pushTokenUpdatedAt.toISOString() : null,\n last_seen_at: device.lastSeenAt ? device.lastSeenAt.toISOString() : null,\n created_at: device.createdAt ? device.createdAt.toISOString() : null,\n updated_at: device.updatedAt ? device.updatedAt.toISOString() : null,\n }\n}\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n const parsedParams = paramsSchema.safeParse({ id: params.id })\n if (!parsedParams.success) {\n return NextResponse.json({ error: translate('devices.errors.invalid_id', 'Invalid device id') }, { status: 400 })\n }\n const device = await loadDevice(container, parsedParams.data.id, auth.tenantId)\n if (!device) {\n return NextResponse.json({ error: translate('devices.errors.not_found', 'Device not found') }, { status: 404 })\n }\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: device.organizationId ?? null })) {\n return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })\n }\n return NextResponse.json({ item: serializeDevice(device) })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.admin.GET failed', { err })\n return NextResponse.json({ error: translate('devices.errors.load_failed', 'Failed to load device') }, { status: 500 })\n }\n}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n const parsedParams = paramsSchema.safeParse({ id: params.id })\n if (!parsedParams.success) {\n return NextResponse.json({ error: translate('devices.errors.invalid_id', 'Invalid device id') }, { status: 400 })\n }\n\n const body = updateDeviceSchema.parse(await readJsonSafe(req, {}))\n const device = await loadDevice(container, parsedParams.data.id, auth.tenantId)\n if (!device) {\n return NextResponse.json({ error: translate('devices.errors.not_found', 'Device not found') }, { status: 404 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: device.organizationId ?? null })) {\n return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })\n }\n const mctx: DeviceMutationContext = {\n container,\n auth,\n scope,\n organizationId: device.organizationId ?? null,\n actorUserId,\n request: req,\n }\n return await executeUpdate(mctx, device, body)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('devices.errors.invalid_payload', 'Invalid device payload'), details: err.flatten() },\n { status: 400 },\n )\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.admin.PUT failed', { err })\n return NextResponse.json({ error: translate('devices.errors.update_failed', 'Failed to update device') }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n const parsedParams = paramsSchema.safeParse({ id: params.id })\n if (!parsedParams.success) {\n return NextResponse.json({ error: translate('devices.errors.invalid_id', 'Invalid device id') }, { status: 400 })\n }\n\n const device = await loadDevice(container, parsedParams.data.id, auth.tenantId)\n if (!device) {\n return NextResponse.json({ error: translate('devices.errors.not_found', 'Device not found') }, { status: 404 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: device.organizationId ?? null })) {\n return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })\n }\n const mctx: DeviceMutationContext = {\n container,\n auth,\n scope,\n organizationId: device.organizationId ?? null,\n actorUserId,\n request: req,\n }\n return await executeDeactivate(mctx, device)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.admin.DELETE failed', { err })\n return NextResponse.json({ error: translate('devices.errors.delete_failed', 'Failed to delete device') }, { status: 500 })\n }\n}\n\nconst okResponseSchema = z.object({ ok: z.literal(true), id: z.string().uuid().optional() })\nconst errorResponseSchema = z.object({ error: z.string() })\nconst detailResponseSchema = z.object({\n item: z.object({\n id: z.string().uuid(),\n user_id: z.string().uuid(),\n device_id: z.string(),\n platform: z.enum(['ios', 'android', 'web']),\n client_app_version: z.string().nullable(),\n os_version: z.string().nullable(),\n push_provider: z.string().nullable(),\n push_token_updated_at: z.string().nullable(),\n last_seen_at: z.string().nullable(),\n created_at: z.string().nullable(),\n updated_at: z.string().nullable(),\n }),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Devices (admin)',\n summary: 'Admin: read, update or deactivate any device in the tenant',\n methods: {\n GET: {\n summary: 'Get any device',\n description: 'Admin: fetch a single device by id (push_token is never returned).',\n responses: [{ status: 200, description: 'Device', schema: detailResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid id', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Missing devices.admin', schema: errorResponseSchema },\n { status: 404, description: 'Device not found', schema: errorResponseSchema },\n ],\n },\n PUT: {\n summary: 'Update any device',\n description: 'Admin: update last-seen, app/OS metadata, and push token for any device. Set pushToken to null to clear it.',\n requestBody: { schema: updateDeviceSchema },\n responses: [{ status: 200, description: 'Device updated', schema: okResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload or id', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Missing devices.admin', schema: errorResponseSchema },\n { status: 404, description: 'Device not found', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Deactivate any device',\n description: 'Admin: soft-delete any device in the tenant.',\n responses: [{ status: 200, description: 'Device deactivated', schema: okResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid id', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Missing devices.admin', schema: errorResponseSchema },\n { status: 404, description: 'Device not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { UserDevice } from '../../../../data/entities'\nimport { updateDeviceSchema } from '../../../../data/validators'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { resolveDeviceActorUserId } from '../../../auth'\nimport { executeUpdate, executeDeactivate, type DeviceMutationContext } from '../../../deviceOps'\nimport { serializeDeviceDetail, deviceDetailItemSchema } from '../../../deviceSerialization'\nimport { DEPRECATED_SNAKE_CASE_NOTICE } from '../../../openapi'\n\nconst logger = createLogger('devices')\n\n// Admin: read/update/deactivate ANY device in the tenant. Gated by devices.admin (no owner check).\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['devices.admin'] },\n PUT: { requireAuth: true, requireFeatures: ['devices.admin'] },\n DELETE: { requireAuth: true, requireFeatures: ['devices.admin'] },\n}\n\nconst paramsSchema = z.object({ id: z.string().uuid() })\n\nasync function loadDevice(\n container: Awaited<ReturnType<typeof createRequestContainer>>,\n id: string,\n tenantId: string,\n): Promise<UserDevice | null> {\n const em = container.resolve('em') as EntityManager\n // push_token is encrypted at rest; the org is not known until the row loads, so pass tenant only \u2014\n // the helper prefers each record's own tenant/org and treats this scope as a fallback.\n return findOneWithDecryption(em, UserDevice, { id, tenantId, deletedAt: null }, undefined, { tenantId })\n}\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n const parsedParams = paramsSchema.safeParse({ id: params.id })\n if (!parsedParams.success) {\n return NextResponse.json({ error: translate('devices.errors.invalid_id', 'Invalid device id') }, { status: 400 })\n }\n const device = await loadDevice(container, parsedParams.data.id, auth.tenantId)\n if (!device) {\n return NextResponse.json({ error: translate('devices.errors.not_found', 'Device not found') }, { status: 404 })\n }\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: device.organizationId ?? null })) {\n return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })\n }\n return NextResponse.json({ item: serializeDeviceDetail(device) })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.admin.GET failed', { err })\n return NextResponse.json({ error: translate('devices.errors.load_failed', 'Failed to load device') }, { status: 500 })\n }\n}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n const parsedParams = paramsSchema.safeParse({ id: params.id })\n if (!parsedParams.success) {\n return NextResponse.json({ error: translate('devices.errors.invalid_id', 'Invalid device id') }, { status: 400 })\n }\n\n const body = updateDeviceSchema.parse(await readJsonSafe(req, {}))\n const device = await loadDevice(container, parsedParams.data.id, auth.tenantId)\n if (!device) {\n return NextResponse.json({ error: translate('devices.errors.not_found', 'Device not found') }, { status: 404 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: device.organizationId ?? null })) {\n return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })\n }\n const mctx: DeviceMutationContext = {\n container,\n auth,\n scope,\n organizationId: device.organizationId ?? null,\n actorUserId,\n request: req,\n }\n return await executeUpdate(mctx, device, body)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('devices.errors.invalid_payload', 'Invalid device payload'), details: err.flatten() },\n { status: 400 },\n )\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.admin.PUT failed', { err })\n return NextResponse.json({ error: translate('devices.errors.update_failed', 'Failed to update device') }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n const parsedParams = paramsSchema.safeParse({ id: params.id })\n if (!parsedParams.success) {\n return NextResponse.json({ error: translate('devices.errors.invalid_id', 'Invalid device id') }, { status: 400 })\n }\n\n const device = await loadDevice(container, parsedParams.data.id, auth.tenantId)\n if (!device) {\n return NextResponse.json({ error: translate('devices.errors.not_found', 'Device not found') }, { status: 404 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: device.organizationId ?? null })) {\n return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })\n }\n const mctx: DeviceMutationContext = {\n container,\n auth,\n scope,\n organizationId: device.organizationId ?? null,\n actorUserId,\n request: req,\n }\n return await executeDeactivate(mctx, device)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.admin.DELETE failed', { err })\n return NextResponse.json({ error: translate('devices.errors.delete_failed', 'Failed to delete device') }, { status: 500 })\n }\n}\n\nconst okResponseSchema = z.object({ ok: z.literal(true), id: z.string().uuid().optional() })\nconst errorResponseSchema = z.object({ error: z.string() })\nconst detailResponseSchema = z.object({ item: deviceDetailItemSchema })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Devices (admin)',\n summary: 'Admin: read, update or deactivate any device in the tenant',\n methods: {\n GET: {\n summary: 'Get any device',\n description: `Admin: fetch a single device by id (push_token is never returned). ${DEPRECATED_SNAKE_CASE_NOTICE}`,\n responses: [{ status: 200, description: 'Device', schema: detailResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid id', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Missing devices.admin', schema: errorResponseSchema },\n { status: 404, description: 'Device not found', schema: errorResponseSchema },\n ],\n },\n PUT: {\n summary: 'Update any device',\n description: 'Admin: update last-seen, app/OS metadata, and push token for any device. Set pushToken to null to clear it.',\n requestBody: { schema: updateDeviceSchema },\n responses: [{ status: 200, description: 'Device updated', schema: okResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload or id', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Missing devices.admin', schema: errorResponseSchema },\n { status: 404, description: 'Device not found', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Deactivate any device',\n description: 'Admin: soft-delete any device in the tenant.',\n responses: [{ status: 200, description: 'Device deactivated', schema: okResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid id', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Missing devices.admin', schema: errorResponseSchema },\n { status: 404, description: 'Device not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
4
+ import { toHeaderLabel } from "@open-mercato/shared/lib/crud/exporters";
4
5
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
6
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
6
7
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
@@ -18,7 +19,14 @@ import {
18
19
  } from "../../../data/validators.js";
19
20
  import { resolveDeviceActorUserId } from "../../auth.js";
20
21
  import { createDevicesCrudOpenApi, createPagedListResponseSchema } from "../../openapi.js";
21
- import { deviceListSchema, deviceListFields, deviceListSortFieldMap, deviceListItemSchema } from "../../deviceList.js";
22
+ import {
23
+ deviceListSchema,
24
+ deviceListFields,
25
+ deviceListSortFieldMap,
26
+ deviceListItemSchema,
27
+ deviceExportColumnFields,
28
+ transformDeviceListItem
29
+ } from "../../deviceList.js";
22
30
  import { executeRegister } from "../../deviceOps.js";
23
31
  const logger = createLogger("devices");
24
32
  const routeMetadata = {
@@ -46,6 +54,11 @@ const crud = makeCrudRoute({
46
54
  entityId: E.devices.user_device,
47
55
  fields: deviceListFields,
48
56
  sortFieldMap: deviceListSortFieldMap,
57
+ // The query engine projects raw column names; expose them in camelCase like every other module.
58
+ transformItem: transformDeviceListItem,
59
+ // Without an explicit column set the factory's default export spreads each item, so the deprecated
60
+ // snake_case aliases would double every column in the CSV/JSON/XML export.
61
+ export: { columns: deviceExportColumnFields.map((field) => ({ field, header: toHeaderLabel(field) })) },
49
62
  // Tenant + org scope is enforced by the factory (orm.tenantField + orm.orgField); only the optional
50
63
  // userId/platform narrowing is left to do here.
51
64
  buildFilters: async (query) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/devices/api/admin/devices/route.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { E } from '#generated/entities.ids.generated'\nimport { UserDevice } from '../../../data/entities'\nimport {\n registerDeviceAdminSchema,\n registerDeviceCommandSchema,\n type RegisterDeviceCommandInput,\n} from '../../../data/validators'\nimport { resolveDeviceActorUserId } from '../../auth'\nimport { createDevicesCrudOpenApi, createPagedListResponseSchema } from '../../openapi'\nimport { deviceListSchema, deviceListFields, deviceListSortFieldMap, deviceListItemSchema } from '../../deviceList'\nimport { executeRegister, type DeviceMutationContext } from '../../deviceOps'\n\nconst logger = createLogger('devices')\n\n// Admin device administration: cross-user listing + register-on-behalf-of-user. Gated by devices.admin.\n// Self-serve registration/listing stays on /api/devices (scoped to the acting user).\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['devices.admin'] },\n POST: { requireAuth: true, requireFeatures: ['devices.admin'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: UserDevice,\n idField: 'id',\n // organization_id stays nullable, but scoping follows the standard org-scoped pattern: the factory\n // auto-filters by the caller's organization scope (null-aware in the query engine). Unrestricted\n // admins see the whole tenant including null-org rows; org-restricted admins see only their orgs.\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.devices.user_device },\n // Same cache resource tag as the self route + commands, so any device write busts both list caches.\n events: { module: 'devices', entity: 'user_device' },\n list: {\n schema: deviceListSchema,\n entityId: E.devices.user_device,\n fields: deviceListFields,\n sortFieldMap: deviceListSortFieldMap,\n // Tenant + org scope is enforced by the factory (orm.tenantField + orm.orgField); only the optional\n // userId/platform narrowing is left to do here.\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (query.userId) filters.user_id = { $eq: query.userId }\n if (query.platform) filters.platform = { $eq: query.platform }\n return filters\n },\n },\n})\n\nexport const GET = crud.GET\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const body = registerDeviceAdminSchema.parse(await readJsonSafe(req, {}))\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n\n // Validate the on-behalf-of target before registering. `body.userId` is caller-supplied, and the\n // fan-out delivers by `(tenant, org, user)` \u2014 so without this an admin could register their OWN push\n // token under an arbitrary `userId` (even a non-existent or cross-tenant one) and start receiving that\n // user's notifications. Require a real, active user in the admin's tenant who is also within the\n // admin's organization scope (org-restricted admins cannot reach users in orgs they can't access).\n // Only non-encrypted columns are selected, so no decryption is needed.\n const em = container.resolve('em') as EntityManager\n const targetUser = await em.findOne(\n User,\n { id: body.userId, tenantId: auth.tenantId, deletedAt: null },\n { fields: ['id', 'organizationId'] },\n )\n if (!targetUser) {\n return NextResponse.json(\n { error: translate('devices.errors.user_not_found', 'Target user not found in this tenant') },\n { status: 400 },\n )\n }\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: targetUser.organizationId ?? null })) {\n return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })\n }\n\n // Register is an idempotent upsert keyed per (tenant, org, user, device). organizationId is the\n // admin's own resolved scope, so the upsert can only ever touch a row inside an org the admin can\n // access \u2014 no cross-org guard is needed here.\n const commandInput = registerDeviceCommandSchema.parse({\n ...body,\n tenantId: auth.tenantId,\n organizationId,\n userId: body.userId,\n } satisfies RegisterDeviceCommandInput)\n\n // actorUserId is the admin (for the mutation guard/audit); the device is owned by body.userId.\n const mctx: DeviceMutationContext = { container, auth, scope, organizationId, actorUserId, request: req }\n return await executeRegister(mctx, commandInput)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('devices.errors.invalid_payload', 'Invalid device payload'), details: err.flatten() },\n { status: 400 },\n )\n }\n if (isCrudHttpError(err) && (err.body as { code?: string } | undefined)?.code === 'device_already_registered') {\n return NextResponse.json(\n { error: translate('devices.errors.already_registered', 'This device is already registered') },\n { status: 409 },\n )\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.admin.POST failed', { err })\n return NextResponse.json(\n { error: translate('devices.errors.register_failed', 'Failed to register device') },\n { status: 500 },\n )\n }\n}\n\nconst registerResponseSchema = z.object({\n id: z.string().uuid(),\n deviceId: z.string(),\n revived: z.boolean(),\n})\n\nexport const openApi: OpenApiRouteDoc = createDevicesCrudOpenApi({\n resourceName: 'Device (admin)',\n pluralName: 'Devices (admin)',\n querySchema: deviceListSchema,\n listResponseSchema: createPagedListResponseSchema(deviceListItemSchema),\n create: {\n schema: registerDeviceAdminSchema,\n responseSchema: registerResponseSchema,\n description: 'Admin: register (idempotently upsert) a device on behalf of any user in the tenant.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { toHeaderLabel } from '@open-mercato/shared/lib/crud/exporters'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { E } from '#generated/entities.ids.generated'\nimport { UserDevice } from '../../../data/entities'\nimport {\n registerDeviceAdminSchema,\n registerDeviceCommandSchema,\n type RegisterDeviceCommandInput,\n} from '../../../data/validators'\nimport { resolveDeviceActorUserId } from '../../auth'\nimport { createDevicesCrudOpenApi, createPagedListResponseSchema } from '../../openapi'\nimport {\n deviceListSchema,\n deviceListFields,\n deviceListSortFieldMap,\n deviceListItemSchema,\n deviceExportColumnFields,\n transformDeviceListItem,\n} from '../../deviceList'\nimport { executeRegister, type DeviceMutationContext } from '../../deviceOps'\n\nconst logger = createLogger('devices')\n\n// Admin device administration: cross-user listing + register-on-behalf-of-user. Gated by devices.admin.\n// Self-serve registration/listing stays on /api/devices (scoped to the acting user).\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['devices.admin'] },\n POST: { requireAuth: true, requireFeatures: ['devices.admin'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: UserDevice,\n idField: 'id',\n // organization_id stays nullable, but scoping follows the standard org-scoped pattern: the factory\n // auto-filters by the caller's organization scope (null-aware in the query engine). Unrestricted\n // admins see the whole tenant including null-org rows; org-restricted admins see only their orgs.\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.devices.user_device },\n // Same cache resource tag as the self route + commands, so any device write busts both list caches.\n events: { module: 'devices', entity: 'user_device' },\n list: {\n schema: deviceListSchema,\n entityId: E.devices.user_device,\n fields: deviceListFields,\n sortFieldMap: deviceListSortFieldMap,\n // The query engine projects raw column names; expose them in camelCase like every other module.\n transformItem: transformDeviceListItem,\n // Without an explicit column set the factory's default export spreads each item, so the deprecated\n // snake_case aliases would double every column in the CSV/JSON/XML export.\n export: { columns: deviceExportColumnFields.map((field) => ({ field, header: toHeaderLabel(field) })) },\n // Tenant + org scope is enforced by the factory (orm.tenantField + orm.orgField); only the optional\n // userId/platform narrowing is left to do here.\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (query.userId) filters.user_id = { $eq: query.userId }\n if (query.platform) filters.platform = { $eq: query.platform }\n return filters\n },\n },\n})\n\nexport const GET = crud.GET\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const body = registerDeviceAdminSchema.parse(await readJsonSafe(req, {}))\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n\n // Validate the on-behalf-of target before registering. `body.userId` is caller-supplied, and the\n // fan-out delivers by `(tenant, org, user)` \u2014 so without this an admin could register their OWN push\n // token under an arbitrary `userId` (even a non-existent or cross-tenant one) and start receiving that\n // user's notifications. Require a real, active user in the admin's tenant who is also within the\n // admin's organization scope (org-restricted admins cannot reach users in orgs they can't access).\n // Only non-encrypted columns are selected, so no decryption is needed.\n const em = container.resolve('em') as EntityManager\n const targetUser = await em.findOne(\n User,\n { id: body.userId, tenantId: auth.tenantId, deletedAt: null },\n { fields: ['id', 'organizationId'] },\n )\n if (!targetUser) {\n return NextResponse.json(\n { error: translate('devices.errors.user_not_found', 'Target user not found in this tenant') },\n { status: 400 },\n )\n }\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: targetUser.organizationId ?? null })) {\n return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })\n }\n\n // Register is an idempotent upsert keyed per (tenant, org, user, device). organizationId is the\n // admin's own resolved scope, so the upsert can only ever touch a row inside an org the admin can\n // access \u2014 no cross-org guard is needed here.\n const commandInput = registerDeviceCommandSchema.parse({\n ...body,\n tenantId: auth.tenantId,\n organizationId,\n userId: body.userId,\n } satisfies RegisterDeviceCommandInput)\n\n // actorUserId is the admin (for the mutation guard/audit); the device is owned by body.userId.\n const mctx: DeviceMutationContext = { container, auth, scope, organizationId, actorUserId, request: req }\n return await executeRegister(mctx, commandInput)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('devices.errors.invalid_payload', 'Invalid device payload'), details: err.flatten() },\n { status: 400 },\n )\n }\n if (isCrudHttpError(err) && (err.body as { code?: string } | undefined)?.code === 'device_already_registered') {\n return NextResponse.json(\n { error: translate('devices.errors.already_registered', 'This device is already registered') },\n { status: 409 },\n )\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.admin.POST failed', { err })\n return NextResponse.json(\n { error: translate('devices.errors.register_failed', 'Failed to register device') },\n { status: 500 },\n )\n }\n}\n\nconst registerResponseSchema = z.object({\n id: z.string().uuid(),\n deviceId: z.string(),\n revived: z.boolean(),\n})\n\nexport const openApi: OpenApiRouteDoc = createDevicesCrudOpenApi({\n resourceName: 'Device (admin)',\n pluralName: 'Devices (admin)',\n querySchema: deviceListSchema,\n listResponseSchema: createPagedListResponseSchema(deviceListItemSchema),\n create: {\n schema: registerDeviceAdminSchema,\n responseSchema: registerResponseSchema,\n description: 'Admin: register (idempotently upsert) a device on behalf of any user in the tenant.',\n },\n})\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,uCAAuC;AAChD,SAAS,YAAY;AACrB,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAEpC,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,gCAAgC;AACzC,SAAS,0BAA0B,qCAAqC;AACxE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAmD;AAE5D,MAAM,SAAS,aAAa,SAAS;AAIrC,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,eAAe,EAAE;AAAA,EAC7D,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,eAAe,EAAE;AAChE;AAEO,MAAM,WAAW;AAExB,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA;AAAA;AAAA;AAAA,IAIT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,SAAS,EAAE,YAAY,EAAE,QAAQ,YAAY;AAAA;AAAA,EAE7C,QAAQ,EAAE,QAAQ,WAAW,QAAQ,cAAc;AAAA,EACnD,MAAM;AAAA,IACJ,QAAQ;AAAA,IACR,UAAU,EAAE,QAAQ;AAAA,IACpB,QAAQ;AAAA,IACR,cAAc;AAAA;AAAA,IAEd,eAAe;AAAA;AAAA;AAAA,IAGf,QAAQ,EAAE,SAAS,yBAAyB,IAAI,CAAC,WAAW,EAAE,OAAO,QAAQ,cAAc,KAAK,EAAE,EAAE,EAAE;AAAA;AAAA;AAAA,IAGtG,cAAc,OAAO,UAAU;AAC7B,YAAM,UAAmC,CAAC;AAC1C,UAAI,MAAM,OAAQ,SAAQ,UAAU,EAAE,KAAK,MAAM,OAAO;AACxD,UAAI,MAAM,SAAU,SAAQ,WAAW,EAAE,KAAK,MAAM,SAAS;AAC7D,aAAO;AAAA,IACT;AAAA,EACF;AACF,CAAC;AAEM,MAAM,MAAM,KAAK;AAExB,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,UAAM,cAAc,yBAAyB,IAAI;AACjD,QAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC,aAAa;AAC3C,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,+BAA+B,cAAc,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC/G;AAEA,UAAM,OAAO,0BAA0B,MAAM,MAAM,aAAa,KAAK,CAAC,CAAC,CAAC;AACxE,UAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,UAAM,iBAAiB,OAAO,cAAc,KAAK,SAAS;AAQ1D,UAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,UAAM,aAAa,MAAM,GAAG;AAAA,MAC1B;AAAA,MACA,EAAE,IAAI,KAAK,QAAQ,UAAU,KAAK,UAAU,WAAW,KAAK;AAAA,MAC5D,EAAE,QAAQ,CAAC,MAAM,gBAAgB,EAAE;AAAA,IACrC;AACA,QAAI,CAAC,YAAY;AACf,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,UAAU,iCAAiC,sCAAsC,EAAE;AAAA,QAC5F,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AACA,QAAI,CAAC,gCAAgC,EAAE,OAAO,MAAM,gBAAgB,WAAW,kBAAkB,KAAK,CAAC,GAAG;AACxG,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,4BAA4B,eAAe,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7G;AAKA,UAAM,eAAe,4BAA4B,MAAM;AAAA,MACrD,GAAG;AAAA,MACH,UAAU,KAAK;AAAA,MACf;AAAA,MACA,QAAQ,KAAK;AAAA,IACf,CAAsC;AAGtC,UAAM,OAA8B,EAAE,WAAW,MAAM,OAAO,gBAAgB,aAAa,SAAS,IAAI;AACxG,WAAO,MAAM,gBAAgB,MAAM,YAAY;AAAA,EACjD,SAAS,KAAK;AACZ,QAAI,eAAe,EAAE,UAAU;AAC7B,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,UAAU,kCAAkC,wBAAwB,GAAG,SAAS,IAAI,QAAQ,EAAE;AAAA,QACvG,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AACA,QAAI,gBAAgB,GAAG,KAAM,IAAI,MAAwC,SAAS,6BAA6B;AAC7G,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,UAAU,qCAAqC,mCAAmC,EAAE;AAAA,QAC7F,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AACA,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,WAAO,MAAM,6BAA6B,EAAE,IAAI,CAAC;AACjD,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,kCAAkC,2BAA2B,EAAE;AAAA,MAClF,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEA,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,UAAU,EAAE,OAAO;AAAA,EACnB,SAAS,EAAE,QAAQ;AACrB,CAAC;AAEM,MAAM,UAA2B,yBAAyB;AAAA,EAC/D,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB,8BAA8B,oBAAoB;AAAA,EACtE,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aAAa;AAAA,EACf;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -23,31 +23,118 @@ const deviceListFields = [
23
23
  "created_at",
24
24
  "updated_at"
25
25
  ];
26
+ const deviceExportColumnFields = [
27
+ "id",
28
+ "tenantId",
29
+ "organizationId",
30
+ "userId",
31
+ "deviceId",
32
+ "platform",
33
+ "clientAppVersion",
34
+ "osVersion",
35
+ "locale",
36
+ "pushProvider",
37
+ "pushTokenUpdatedAt",
38
+ "lastSeenAt",
39
+ "createdAt",
40
+ "updatedAt"
41
+ ];
26
42
  const deviceListSortFieldMap = {
27
43
  lastSeenAt: "last_seen_at",
28
44
  createdAt: "created_at",
29
45
  updatedAt: "updated_at"
30
46
  };
47
+ function transformDeviceListItem(item) {
48
+ const record = toRecord(item);
49
+ if (!Object.keys(record).length) return item;
50
+ const camel = {
51
+ id: readString(record, "id", "id"),
52
+ tenantId: readString(record, "tenant_id", "tenantId"),
53
+ organizationId: readString(record, "organization_id", "organizationId"),
54
+ userId: readString(record, "user_id", "userId"),
55
+ deviceId: readString(record, "device_id", "deviceId"),
56
+ platform: readString(record, "platform", "platform"),
57
+ clientAppVersion: readString(record, "client_app_version", "clientAppVersion"),
58
+ osVersion: readString(record, "os_version", "osVersion"),
59
+ locale: readString(record, "locale", "locale"),
60
+ pushProvider: readString(record, "push_provider", "pushProvider"),
61
+ pushTokenUpdatedAt: toIso(record.push_token_updated_at ?? record.pushTokenUpdatedAt),
62
+ lastSeenAt: toIso(record.last_seen_at ?? record.lastSeenAt),
63
+ createdAt: toIso(record.created_at ?? record.createdAt),
64
+ updatedAt: toIso(record.updated_at ?? record.updatedAt)
65
+ };
66
+ return { ...record, ...camel, ...toDeprecatedSnakeCaseAliases(camel) };
67
+ }
68
+ function toDeprecatedSnakeCaseAliases(item) {
69
+ const aliases = {
70
+ tenant_id: item.tenantId,
71
+ organization_id: item.organizationId,
72
+ user_id: item.userId,
73
+ device_id: item.deviceId,
74
+ client_app_version: item.clientAppVersion,
75
+ os_version: item.osVersion,
76
+ push_provider: item.pushProvider,
77
+ push_token_updated_at: item.pushTokenUpdatedAt,
78
+ last_seen_at: item.lastSeenAt,
79
+ created_at: item.createdAt,
80
+ updated_at: item.updatedAt
81
+ };
82
+ for (const key of Object.keys(aliases)) {
83
+ if (aliases[key] === void 0) delete aliases[key];
84
+ }
85
+ return aliases;
86
+ }
87
+ function toRecord(value) {
88
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
89
+ }
90
+ function readString(record, snakeKey, camelKey) {
91
+ const value = record[snakeKey] ?? record[camelKey];
92
+ return typeof value === "string" ? value : null;
93
+ }
94
+ function toIso(value) {
95
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
96
+ if (typeof value === "string") {
97
+ const date = new Date(value);
98
+ return Number.isNaN(date.getTime()) ? value : date.toISOString();
99
+ }
100
+ return null;
101
+ }
102
+ const deprecatedAlias = (of) => `Deprecated alias for \`${of}\`; removed in the next minor release.`;
31
103
  const deviceListItemSchema = z.object({
32
104
  id: z.string().uuid(),
33
- tenant_id: z.string().uuid(),
34
- organization_id: z.string().uuid().nullable().optional(),
35
- user_id: z.string().uuid(),
36
- device_id: z.string(),
105
+ tenantId: z.string().uuid(),
106
+ organizationId: z.string().uuid().nullable().optional(),
107
+ userId: z.string().uuid(),
108
+ deviceId: z.string(),
37
109
  platform: z.enum(["ios", "android", "web"]),
38
- client_app_version: z.string().nullable().optional(),
39
- os_version: z.string().nullable().optional(),
110
+ clientAppVersion: z.string().nullable().optional(),
111
+ osVersion: z.string().nullable().optional(),
40
112
  locale: z.string().nullable().optional(),
41
- push_provider: z.string().nullable().optional(),
42
- push_token_updated_at: z.string().nullable().optional(),
43
- last_seen_at: z.string().nullable().optional(),
44
- created_at: z.string().nullable().optional(),
45
- updated_at: z.string().nullable().optional()
113
+ pushProvider: z.string().nullable().optional(),
114
+ pushTokenUpdatedAt: z.string().nullable().optional(),
115
+ lastSeenAt: z.string().nullable().optional(),
116
+ createdAt: z.string().nullable().optional(),
117
+ updatedAt: z.string().nullable().optional(),
118
+ tenant_id: z.string().uuid().describe(deprecatedAlias("tenantId")),
119
+ organization_id: z.string().uuid().nullable().optional().describe(deprecatedAlias("organizationId")),
120
+ user_id: z.string().uuid().describe(deprecatedAlias("userId")),
121
+ device_id: z.string().describe(deprecatedAlias("deviceId")),
122
+ client_app_version: z.string().nullable().optional().describe(deprecatedAlias("clientAppVersion")),
123
+ os_version: z.string().nullable().optional().describe(deprecatedAlias("osVersion")),
124
+ push_provider: z.string().nullable().optional().describe(deprecatedAlias("pushProvider")),
125
+ push_token_updated_at: z.string().nullable().optional().describe(deprecatedAlias("pushTokenUpdatedAt")),
126
+ last_seen_at: z.string().nullable().optional().describe(deprecatedAlias("lastSeenAt")),
127
+ created_at: z.string().nullable().optional().describe(deprecatedAlias("createdAt")),
128
+ updated_at: z.string().nullable().optional().describe(deprecatedAlias("updatedAt"))
46
129
  });
47
130
  export {
131
+ deviceExportColumnFields,
48
132
  deviceListFields,
49
133
  deviceListItemSchema,
50
134
  deviceListSchema,
51
- deviceListSortFieldMap
135
+ deviceListSortFieldMap,
136
+ toDeprecatedSnakeCaseAliases,
137
+ toIso,
138
+ transformDeviceListItem
52
139
  };
53
140
  //# sourceMappingURL=deviceList.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/devices/api/deviceList.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\n\n// Shared list contract for the self-serve (`/api/devices`) and admin (`/api/devices/admin/devices`)\n// list routes. push_token is a secret and is never part of the exposed field set.\nexport const deviceListSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n platform: z.enum(['ios', 'android', 'web']).optional(),\n userId: z.string().uuid().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\nexport const deviceListFields: string[] = [\n 'id',\n 'tenant_id',\n 'organization_id',\n 'user_id',\n 'device_id',\n 'platform',\n 'client_app_version',\n 'os_version',\n 'locale',\n 'push_provider',\n 'push_token_updated_at',\n 'last_seen_at',\n 'created_at',\n 'updated_at',\n]\n\nexport const deviceListSortFieldMap: Record<string, string> = {\n lastSeenAt: 'last_seen_at',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n}\n\nexport const deviceListItemSchema = z.object({\n id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n organization_id: z.string().uuid().nullable().optional(),\n user_id: z.string().uuid(),\n device_id: z.string(),\n platform: z.enum(['ios', 'android', 'web']),\n client_app_version: z.string().nullable().optional(),\n os_version: z.string().nullable().optional(),\n locale: z.string().nullable().optional(),\n push_provider: z.string().nullable().optional(),\n push_token_updated_at: z.string().nullable().optional(),\n last_seen_at: z.string().nullable().optional(),\n created_at: z.string().nullable().optional(),\n updated_at: z.string().nullable().optional(),\n})\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAIX,MAAM,mBAAmB,EAC7B,OAAO;AAAA,EACN,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,UAAU,EAAE,KAAK,CAAC,OAAO,WAAW,KAAK,CAAC,EAAE,SAAS;AAAA,EACrD,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACnC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAC5C,CAAC,EACA,YAAY;AAER,MAAM,mBAA6B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,MAAM,yBAAiD;AAAA,EAC5D,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,WAAW;AACb;AAEO,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACvD,SAAS,EAAE,OAAO,EAAE,KAAK;AAAA,EACzB,WAAW,EAAE,OAAO;AAAA,EACpB,UAAU,EAAE,KAAK,CAAC,OAAO,WAAW,KAAK,CAAC;AAAA,EAC1C,oBAAoB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACnD,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC3C,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACvC,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,uBAAuB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC7C,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC3C,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC7C,CAAC;",
4
+ "sourcesContent": ["import { z } from 'zod'\n\n// Shared list contract for the self-serve (`/api/devices`) and admin (`/api/devices/admin/devices`)\n// list routes. push_token is a secret and is never part of the exposed field set.\nexport const deviceListSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n platform: z.enum(['ios', 'android', 'web']).optional(),\n userId: z.string().uuid().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\nexport const deviceListFields: string[] = [\n 'id',\n 'tenant_id',\n 'organization_id',\n 'user_id',\n 'device_id',\n 'platform',\n 'client_app_version',\n 'os_version',\n 'locale',\n 'push_provider',\n 'push_token_updated_at',\n 'last_seen_at',\n 'created_at',\n 'updated_at',\n]\n\n// The admin list enables exports, and the factory's default export derives its columns by spreading\n// each item \u2014 which would emit both spellings of every aliased key. Pin the canonical camelCase set\n// so the export stays single-spelled and does not change shape when the aliases are dropped.\nexport const deviceExportColumnFields: string[] = [\n 'id',\n 'tenantId',\n 'organizationId',\n 'userId',\n 'deviceId',\n 'platform',\n 'clientAppVersion',\n 'osVersion',\n 'locale',\n 'pushProvider',\n 'pushTokenUpdatedAt',\n 'lastSeenAt',\n 'createdAt',\n 'updatedAt',\n]\n\nexport const deviceListSortFieldMap: Record<string, string> = {\n lastSeenAt: 'last_seen_at',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n}\n\n// The query engine projects the raw column names listed in `deviceListFields`. Every other module\n// serializes its list items in camelCase (`sales/orders` \u2192 `orderNumber`, `warranty_claims` \u2192\n// `claimNumber`), so the list routes run this transform to expose the platform convention (#5513).\n// The snake_case keys stay alongside as deprecated aliases for one minor version, per the\n// deprecation protocol in BACKWARD_COMPATIBILITY.md \u00A7 7 (API Route URLs \u2014 response fields).\nexport function transformDeviceListItem(item: unknown): unknown {\n const record = toRecord(item)\n if (!Object.keys(record).length) return item\n const camel = {\n id: readString(record, 'id', 'id'),\n tenantId: readString(record, 'tenant_id', 'tenantId'),\n organizationId: readString(record, 'organization_id', 'organizationId'),\n userId: readString(record, 'user_id', 'userId'),\n deviceId: readString(record, 'device_id', 'deviceId'),\n platform: readString(record, 'platform', 'platform'),\n clientAppVersion: readString(record, 'client_app_version', 'clientAppVersion'),\n osVersion: readString(record, 'os_version', 'osVersion'),\n locale: readString(record, 'locale', 'locale'),\n pushProvider: readString(record, 'push_provider', 'pushProvider'),\n pushTokenUpdatedAt: toIso(record.push_token_updated_at ?? record.pushTokenUpdatedAt),\n lastSeenAt: toIso(record.last_seen_at ?? record.lastSeenAt),\n createdAt: toIso(record.created_at ?? record.createdAt),\n updatedAt: toIso(record.updated_at ?? record.updatedAt),\n }\n // Spread the raw record first so anything the projection carries beyond the declared field set\n // (custom-field keys, future columns) survives; the aliases then restate the snake_case keys with\n // the same normalized values as their camelCase counterparts.\n return { ...record, ...camel, ...toDeprecatedSnakeCaseAliases(camel) }\n}\n\n/**\n * @deprecated Snake_case device response keys are superseded by the camelCase keys and are removed in\n * the next minor release. Read `deviceId`, `userId`, `lastSeenAt`, \u2026 instead. See UPGRADE_NOTES.md.\n */\nexport function toDeprecatedSnakeCaseAliases(item: DeviceResponseItem): Record<string, unknown> {\n const aliases: Record<string, unknown> = {\n tenant_id: item.tenantId,\n organization_id: item.organizationId,\n user_id: item.userId,\n device_id: item.deviceId,\n client_app_version: item.clientAppVersion,\n os_version: item.osVersion,\n push_provider: item.pushProvider,\n push_token_updated_at: item.pushTokenUpdatedAt,\n last_seen_at: item.lastSeenAt,\n created_at: item.createdAt,\n updated_at: item.updatedAt,\n }\n // The detail response never carried tenant/org, so an absent camelCase source must not grow a new\n // key here \u2014 only keys the caller actually supplied get an alias.\n for (const key of Object.keys(aliases)) {\n if (aliases[key] === undefined) delete aliases[key]\n }\n return aliases\n}\n\nexport type DeviceResponseItem = {\n tenantId?: string | null\n organizationId?: string | null\n userId: string | null\n deviceId: string | null\n clientAppVersion: string | null\n osVersion: string | null\n pushProvider: string | null\n pushTokenUpdatedAt?: string | null\n lastSeenAt?: string | null\n createdAt?: string | null\n updatedAt: string | null\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {}\n}\n\nfunction readString(record: Record<string, unknown>, snakeKey: string, camelKey: string): string | null {\n const value = record[snakeKey] ?? record[camelKey]\n return typeof value === 'string' ? value : null\n}\n\nexport function toIso(value: unknown): string | null {\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString()\n if (typeof value === 'string') {\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? value : date.toISOString()\n }\n return null\n}\n\n// In-code contract markers. The shared zodToJsonSchema converter does not currently emit per-property\n// descriptions for object schemas, so the rendered deprecation notice lives on the endpoint description\n// (`DEPRECATED_SNAKE_CASE_NOTICE` in ./openapi); these become visible for free if that changes.\nconst deprecatedAlias = (of: string) => `Deprecated alias for \\`${of}\\`; removed in the next minor release.`\n\nexport const deviceListItemSchema = z.object({\n id: z.string().uuid(),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable().optional(),\n userId: z.string().uuid(),\n deviceId: z.string(),\n platform: z.enum(['ios', 'android', 'web']),\n clientAppVersion: z.string().nullable().optional(),\n osVersion: z.string().nullable().optional(),\n locale: z.string().nullable().optional(),\n pushProvider: z.string().nullable().optional(),\n pushTokenUpdatedAt: z.string().nullable().optional(),\n lastSeenAt: z.string().nullable().optional(),\n createdAt: z.string().nullable().optional(),\n updatedAt: z.string().nullable().optional(),\n tenant_id: z.string().uuid().describe(deprecatedAlias('tenantId')),\n organization_id: z.string().uuid().nullable().optional().describe(deprecatedAlias('organizationId')),\n user_id: z.string().uuid().describe(deprecatedAlias('userId')),\n device_id: z.string().describe(deprecatedAlias('deviceId')),\n client_app_version: z.string().nullable().optional().describe(deprecatedAlias('clientAppVersion')),\n os_version: z.string().nullable().optional().describe(deprecatedAlias('osVersion')),\n push_provider: z.string().nullable().optional().describe(deprecatedAlias('pushProvider')),\n push_token_updated_at: z.string().nullable().optional().describe(deprecatedAlias('pushTokenUpdatedAt')),\n last_seen_at: z.string().nullable().optional().describe(deprecatedAlias('lastSeenAt')),\n created_at: z.string().nullable().optional().describe(deprecatedAlias('createdAt')),\n updated_at: z.string().nullable().optional().describe(deprecatedAlias('updatedAt')),\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { toDeprecatedSnakeCaseAliases, toIso } from "./deviceList.js";
3
+ function serializeDeviceDetail(device) {
4
+ const camel = {
5
+ id: device.id,
6
+ userId: device.userId,
7
+ deviceId: device.deviceId,
8
+ platform: device.platform,
9
+ clientAppVersion: device.clientAppVersion ?? null,
10
+ osVersion: device.osVersion ?? null,
11
+ pushProvider: device.pushProvider ?? null,
12
+ pushTokenUpdatedAt: toIso(device.pushTokenUpdatedAt),
13
+ lastSeenAt: toIso(device.lastSeenAt),
14
+ createdAt: toIso(device.createdAt),
15
+ updatedAt: toIso(device.updatedAt)
16
+ };
17
+ return { ...camel, ...toDeprecatedSnakeCaseAliases(camel) };
18
+ }
19
+ const deprecatedAlias = (of) => `Deprecated alias for \`${of}\`; removed in the next minor release.`;
20
+ const deviceDetailItemSchema = z.object({
21
+ id: z.string().uuid(),
22
+ userId: z.string().uuid(),
23
+ deviceId: z.string(),
24
+ platform: z.enum(["ios", "android", "web"]),
25
+ clientAppVersion: z.string().nullable(),
26
+ osVersion: z.string().nullable(),
27
+ pushProvider: z.string().nullable(),
28
+ pushTokenUpdatedAt: z.string().nullable(),
29
+ lastSeenAt: z.string().nullable(),
30
+ createdAt: z.string().nullable(),
31
+ updatedAt: z.string().nullable(),
32
+ user_id: z.string().uuid().describe(deprecatedAlias("userId")),
33
+ device_id: z.string().describe(deprecatedAlias("deviceId")),
34
+ client_app_version: z.string().nullable().describe(deprecatedAlias("clientAppVersion")),
35
+ os_version: z.string().nullable().describe(deprecatedAlias("osVersion")),
36
+ push_provider: z.string().nullable().describe(deprecatedAlias("pushProvider")),
37
+ push_token_updated_at: z.string().nullable().describe(deprecatedAlias("pushTokenUpdatedAt")),
38
+ last_seen_at: z.string().nullable().describe(deprecatedAlias("lastSeenAt")),
39
+ created_at: z.string().nullable().describe(deprecatedAlias("createdAt")),
40
+ updated_at: z.string().nullable().describe(deprecatedAlias("updatedAt"))
41
+ });
42
+ export {
43
+ deviceDetailItemSchema,
44
+ serializeDeviceDetail
45
+ };
46
+ //# sourceMappingURL=deviceSerialization.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/devices/api/deviceSerialization.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { UserDevice } from '../data/entities'\nimport { toDeprecatedSnakeCaseAliases, toIso } from './deviceList'\n\n// Detail contract for `GET /api/devices/admin/devices/:id`. Keys are camelCase like every other\n// module's responses (#5513); the snake_case keys stay alongside as deprecated aliases for one minor\n// version, per the deprecation protocol in BACKWARD_COMPATIBILITY.md \u00A7 7.\n// push_token is a secret and is never returned.\nexport function serializeDeviceDetail(device: UserDevice): Record<string, unknown> {\n const camel = {\n id: device.id,\n userId: device.userId,\n deviceId: device.deviceId,\n platform: device.platform,\n clientAppVersion: device.clientAppVersion ?? null,\n osVersion: device.osVersion ?? null,\n pushProvider: device.pushProvider ?? null,\n pushTokenUpdatedAt: toIso(device.pushTokenUpdatedAt),\n lastSeenAt: toIso(device.lastSeenAt),\n createdAt: toIso(device.createdAt),\n updatedAt: toIso(device.updatedAt),\n }\n return { ...camel, ...toDeprecatedSnakeCaseAliases(camel) }\n}\n\n// See the note in ./deviceList \u2014 the rendered deprecation notice lives on the endpoint description.\nconst deprecatedAlias = (of: string) => `Deprecated alias for \\`${of}\\`; removed in the next minor release.`\n\nexport const deviceDetailItemSchema = z.object({\n id: z.string().uuid(),\n userId: z.string().uuid(),\n deviceId: z.string(),\n platform: z.enum(['ios', 'android', 'web']),\n clientAppVersion: z.string().nullable(),\n osVersion: z.string().nullable(),\n pushProvider: z.string().nullable(),\n pushTokenUpdatedAt: z.string().nullable(),\n lastSeenAt: z.string().nullable(),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n user_id: z.string().uuid().describe(deprecatedAlias('userId')),\n device_id: z.string().describe(deprecatedAlias('deviceId')),\n client_app_version: z.string().nullable().describe(deprecatedAlias('clientAppVersion')),\n os_version: z.string().nullable().describe(deprecatedAlias('osVersion')),\n push_provider: z.string().nullable().describe(deprecatedAlias('pushProvider')),\n push_token_updated_at: z.string().nullable().describe(deprecatedAlias('pushTokenUpdatedAt')),\n last_seen_at: z.string().nullable().describe(deprecatedAlias('lastSeenAt')),\n created_at: z.string().nullable().describe(deprecatedAlias('createdAt')),\n updated_at: z.string().nullable().describe(deprecatedAlias('updatedAt')),\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAElB,SAAS,8BAA8B,aAAa;AAM7C,SAAS,sBAAsB,QAA6C;AACjF,QAAM,QAAQ;AAAA,IACZ,IAAI,OAAO;AAAA,IACX,QAAQ,OAAO;AAAA,IACf,UAAU,OAAO;AAAA,IACjB,UAAU,OAAO;AAAA,IACjB,kBAAkB,OAAO,oBAAoB;AAAA,IAC7C,WAAW,OAAO,aAAa;AAAA,IAC/B,cAAc,OAAO,gBAAgB;AAAA,IACrC,oBAAoB,MAAM,OAAO,kBAAkB;AAAA,IACnD,YAAY,MAAM,OAAO,UAAU;AAAA,IACnC,WAAW,MAAM,OAAO,SAAS;AAAA,IACjC,WAAW,MAAM,OAAO,SAAS;AAAA,EACnC;AACA,SAAO,EAAE,GAAG,OAAO,GAAG,6BAA6B,KAAK,EAAE;AAC5D;AAGA,MAAM,kBAAkB,CAAC,OAAe,0BAA0B,EAAE;AAE7D,MAAM,yBAAyB,EAAE,OAAO;AAAA,EAC7C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,UAAU,EAAE,OAAO;AAAA,EACnB,UAAU,EAAE,KAAK,CAAC,OAAO,WAAW,KAAK,CAAC;AAAA,EAC1C,kBAAkB,EAAE,OAAO,EAAE,SAAS;AAAA,EACtC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,oBAAoB,EAAE,OAAO,EAAE,SAAS;AAAA,EACxC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,gBAAgB,QAAQ,CAAC;AAAA,EAC7D,WAAW,EAAE,OAAO,EAAE,SAAS,gBAAgB,UAAU,CAAC;AAAA,EAC1D,oBAAoB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,gBAAgB,kBAAkB,CAAC;AAAA,EACtF,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,gBAAgB,WAAW,CAAC;AAAA,EACvE,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,gBAAgB,cAAc,CAAC;AAAA,EAC7E,uBAAuB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,gBAAgB,oBAAoB,CAAC;AAAA,EAC3F,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,gBAAgB,YAAY,CAAC;AAAA,EAC1E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,gBAAgB,WAAW,CAAC;AAAA,EACvE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,gBAAgB,WAAW,CAAC;AACzE,CAAC;",
6
+ "names": []
7
+ }
@@ -5,14 +5,16 @@ import {
5
5
  function createPagedListResponseSchema(itemSchema) {
6
6
  return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true });
7
7
  }
8
+ const DEPRECATED_SNAKE_CASE_NOTICE = "Response keys are camelCase. The snake_case keys (`user_id`, `device_id`, `last_seen_at`, \u2026) are deprecated aliases of their camelCase counterparts, retained for one minor version and removed in the next; read `userId`, `deviceId`, `lastSeenAt`, \u2026 instead.";
8
9
  const buildDevicesCrudOpenApi = createCrudOpenApiFactory({
9
10
  defaultTag: "Devices",
10
- makeListDescription: ({ pluralLower }) => `Returns the authenticated user's registered ${pluralLower} (admins may list across users).`
11
+ makeListDescription: ({ pluralLower }) => `Returns the authenticated user's registered ${pluralLower} (admins may list across users). ${DEPRECATED_SNAKE_CASE_NOTICE}`
11
12
  });
12
13
  function createDevicesCrudOpenApi(options) {
13
14
  return buildDevicesCrudOpenApi(options);
14
15
  }
15
16
  export {
17
+ DEPRECATED_SNAKE_CASE_NOTICE,
16
18
  createDevicesCrudOpenApi,
17
19
  createPagedListResponseSchema
18
20
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/devices/api/openapi.ts"],
4
- "sourcesContent": ["import { type ZodTypeAny } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n createCrudOpenApiFactory,\n createPagedListResponseSchema as createSharedPagedListResponseSchema,\n type CrudOpenApiOptions,\n} from '@open-mercato/shared/lib/openapi/crud'\n\nexport function createPagedListResponseSchema(itemSchema: ZodTypeAny) {\n return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })\n}\n\n// createCrudOpenApiFactory already falls back to the shared default create/ok response schemas\n// when omitted, so there is nothing module-specific to re-pass or re-export here.\nconst buildDevicesCrudOpenApi = createCrudOpenApiFactory({\n defaultTag: 'Devices',\n makeListDescription: ({ pluralLower }) =>\n `Returns the authenticated user's registered ${pluralLower} (admins may list across users).`,\n})\n\nexport function createDevicesCrudOpenApi(options: CrudOpenApiOptions): OpenApiRouteDoc {\n return buildDevicesCrudOpenApi(options)\n}\n"],
5
- "mappings": "AAEA;AAAA,EACE;AAAA,EACA,iCAAiC;AAAA,OAE5B;AAEA,SAAS,8BAA8B,YAAwB;AACpE,SAAO,oCAAoC,YAAY,EAAE,wBAAwB,KAAK,CAAC;AACzF;AAIA,MAAM,0BAA0B,yBAAyB;AAAA,EACvD,YAAY;AAAA,EACZ,qBAAqB,CAAC,EAAE,YAAY,MAClC,+CAA+C,WAAW;AAC9D,CAAC;AAEM,SAAS,yBAAyB,SAA8C;AACrF,SAAO,wBAAwB,OAAO;AACxC;",
4
+ "sourcesContent": ["import { type ZodTypeAny } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n createCrudOpenApiFactory,\n createPagedListResponseSchema as createSharedPagedListResponseSchema,\n type CrudOpenApiOptions,\n} from '@open-mercato/shared/lib/openapi/crud'\n\nexport function createPagedListResponseSchema(itemSchema: ZodTypeAny) {\n return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })\n}\n\n// createCrudOpenApiFactory already falls back to the shared default create/ok response schemas\n// when omitted, so there is nothing module-specific to re-pass or re-export here.\n// The shared zodToJsonSchema converter does not emit per-property descriptions for object schemas, so\n// the `.describe()` markers on the deprecated keys in deviceList.ts never reach the document. State the\n// deprecation in the endpoint description, which does render (#5513).\nexport const DEPRECATED_SNAKE_CASE_NOTICE =\n 'Response keys are camelCase. The snake_case keys (`user_id`, `device_id`, `last_seen_at`, \u2026) are ' +\n 'deprecated aliases of their camelCase counterparts, retained for one minor version and removed in ' +\n 'the next; read `userId`, `deviceId`, `lastSeenAt`, \u2026 instead.'\n\nconst buildDevicesCrudOpenApi = createCrudOpenApiFactory({\n defaultTag: 'Devices',\n makeListDescription: ({ pluralLower }) =>\n `Returns the authenticated user's registered ${pluralLower} (admins may list across users). ${DEPRECATED_SNAKE_CASE_NOTICE}`,\n})\n\nexport function createDevicesCrudOpenApi(options: CrudOpenApiOptions): OpenApiRouteDoc {\n return buildDevicesCrudOpenApi(options)\n}\n"],
5
+ "mappings": "AAEA;AAAA,EACE;AAAA,EACA,iCAAiC;AAAA,OAE5B;AAEA,SAAS,8BAA8B,YAAwB;AACpE,SAAO,oCAAoC,YAAY,EAAE,wBAAwB,KAAK,CAAC;AACzF;AAOO,MAAM,+BACX;AAIF,MAAM,0BAA0B,yBAAyB;AAAA,EACvD,YAAY;AAAA,EACZ,qBAAqB,CAAC,EAAE,YAAY,MAClC,+CAA+C,WAAW,oCAAoC,4BAA4B;AAC9H,CAAC;AAEM,SAAS,yBAAyB,SAA8C;AACrF,SAAO,wBAAwB,OAAO;AACxC;",
6
6
  "names": []
7
7
  }
@@ -18,7 +18,13 @@ import {
18
18
  } from "../data/validators.js";
19
19
  import { resolveDeviceActorUserId } from "./auth.js";
20
20
  import { createDevicesCrudOpenApi, createPagedListResponseSchema } from "./openapi.js";
21
- import { deviceListSchema, deviceListFields, deviceListSortFieldMap, deviceListItemSchema } from "./deviceList.js";
21
+ import {
22
+ deviceListSchema,
23
+ deviceListFields,
24
+ deviceListSortFieldMap,
25
+ deviceListItemSchema,
26
+ transformDeviceListItem
27
+ } from "./deviceList.js";
22
28
  import { executeRegister } from "./deviceOps.js";
23
29
  const logger = createLogger("devices");
24
30
  const routeMetadata = {
@@ -52,6 +58,8 @@ const crud = makeCrudRoute({
52
58
  // push_token is a secret and is never exposed via the list API.
53
59
  fields: deviceListFields,
54
60
  sortFieldMap: deviceListSortFieldMap,
61
+ // The query engine projects raw column names; expose them in camelCase like every other module.
62
+ transformItem: transformDeviceListItem,
55
63
  // Exports are off for the self-serve list. The factory enables them for every list that does not
56
64
  // declare `export` (resolveAvailableExportFormats falls back to csv/json/xml/markdown), and its
57
65
  // full-export branch replaces the filters with `{}` instead of calling `buildFilters`