// @ts-check
/**
* Project/Share wire-format helpers for the Members service contract
* (see 080-Workspaces/018-Cross-Service-Contracts.mdx).
*
* The internal storage follows the members model:
* - Project: ObjectBase row with Type='project', UIDBelongsTo=organization,
* Title/Display, Data = { description, metadata, ... }
* - Share: ObjectBase row with Type='repositoryShare'|'directoryShare',
* UIDBelongsTo=organization UID, Data = resource metadata
* - Relation: Links rows with Type='memberA'|'member',
* UID=project, UIDTarget=share
* ('memberA' = write/admin, 'member' = read only)
*
* Share objects never carry an ACL and never reference secret values.
*/
import { HEX2uuid } from '@commtool/sql-query';
/** @typedef {'repositoryShare'|'directoryShare'} ShareType */
export const SHARE_TYPES = /** @type {const} */ (['repositoryShare', 'directoryShare']);
/** MariaDB temporal history guarantee (contract: 13 years). */
export const RETENTION_YEARS = 13;
/**
* Format a DB timestamp (Date, string or number) as RFC3339 with microsecond
* precision (the contract's API time format).
* @param {Date|string|number} value
* @returns {string}
*/
export const toRfc3339Micro = (value) => {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return '1970-01-01T00:00:00.000000Z';
}
const iso = date.toISOString(); // YYYY-MM-DDTHH:mm:ss.sssZ
const micros = String(date.getMilliseconds()).padStart(3, '0') + '000';
return iso.replace(/\.\d{3}Z$/, `.${micros}Z`);
};
/**
* Map an ObjectBase row to the contract Project object.
* @param {Object} row - raw ObjectBase/Member row (UUID cast applied)
* @returns {Object}
*/
export const projectToContract = (row) => ({
uid: row.UID,
organization_uid: row.organization_uid || row.UIDBelongsTo,
group_uid: row.group_uid || null,
title: row.Title || row.Display || '',
description: row.description ?? null,
metadata: row.metadata ?? {},
source_updated_at: toRfc3339Micro(row.source_updated_at ?? row.ValidFrom ?? new Date()),
});
/**
* Map an ObjectBase row to the contract Share object. The share belongs to the
* org, so the contract `project_uid` is taken from the project context that
* resolved the share (via memberA/member link), never from UIDBelongsTo.
* @param {Object} row - raw ObjectBase row (UUID cast applied)
* @param {string} [projectUid] - project that links this share
* @returns {Object}
*/
export const shareToContract = (row, projectUid = null) => ({
uid: row.UID,
project_uid: projectUid || row.project_uid || null,
type: row.Type,
metadata: row.metadata ?? {},
source_updated_at: toRfc3339Micro(row.source_updated_at ?? row.ValidFrom ?? new Date()),
});
/**
* Extract the metadata object from a project row's `metadata_json` envelope
* (`JSON_OBJECT('value', JSON_EXTRACT(Data, '$.metadata'))`).
*
* With `{ cast: ['json'] }` the driver already parses the envelope into an
* object (`{ value: {...} }`), while legacy/test call paths may still deliver a
* JSON string. Both forms are normalized here; anything else yields `{}`.
* @param {Object} row
* @returns {Object}
*/
export const metadataFromRow = (row) => {
let envelope = row && row.metadata_json;
if (!envelope) return {};
if (typeof envelope === 'string') {
try {
envelope = JSON.parse(envelope);
} catch {
return {};
}
}
const value = envelope && typeof envelope === 'object' ? envelope.value : null;
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
};
/**
* Map a raw ObjectBase row to the contract Share object for a share that also
* carries a memberA/member link (used by snapshot reads over system time).
* @param {Object} row
* @param {string} projectUid
* @param {string} [linkType] - 'memberA'|'member'
* @returns {Object}
*/
export const shareLinkToContract = (row, projectUid, linkType = null) => {
let data = row.Data;
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch {
data = null;
}
}
return {
uid: HEX2uuid(row.UID),
project_uid: projectUid,
type: row.Type,
linkType: linkType === 'memberA' ? 'write' : linkType === 'member' ? 'read' : null,
metadata: data && typeof data === 'object' ? data.metadata ?? {} : {},
source_updated_at: toRfc3339Micro(row.ValidFrom),
};
};