Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 51 additions & 24 deletions src/commands/members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ const printMemberPreview = (member: Member): void => {

const fetchAllMembers = async (
spinner: ReturnType<typeof yoctoSpinner>,
order = "ASC",
filters?: Record<string, unknown>
): Promise<{ members: Member[]; totalCount: number }> => {
const allMembers: Member[] = [];
Expand All @@ -74,22 +73,24 @@ const fetchAllMembers = async (
do {
const result = await graphqlRequest<{
getMembers: {
edges: { cursor: string; node: Member }[];
edges: { node: Member }[];
pageInfo: { endCursor: string | null };
};
}>({
query: `query($first: Int, $after: String, $order: OrderByInput, $filters: MemberFilter) {
getMembers(first: $first, after: $after, order: $order, filters: $filters) {
edges { cursor node { ${MEMBER_FIELDS} } }
query: `query($first: Int, $after: String, $filters: MemberFilter) {
getMembers(first: $first, after: $after, filters: $filters) {
edges { node { ${MEMBER_FIELDS} } }
pageInfo { endCursor }
}
}`,
variables: { first: pageSize, after: cursor, order, filters },
variables: { first: pageSize, after: cursor, filters },
});

const { edges } = result.getMembers;
const { edges, pageInfo } = result.getMembers;
allMembers.push(...edges.map((e) => e.node));

if (edges.length === pageSize) {
cursor = edges.at(-1)?.cursor;
if (edges.length === pageSize && pageInfo.endCursor) {
cursor = pageInfo.endCursor;
spinner.text = `Fetching members... (${allMembers.length} so far)`;
} else {
cursor = undefined;
Expand All @@ -107,8 +108,12 @@ const flattenMember = (member: Member): Record<string, unknown> => ({
createdAt: member.createdAt,
lastLogin: member.lastLogin ?? "",
loginRedirect: member.loginRedirect ?? "",
permissions: member.permissions.all.join(", "),
plans: member.planConnections.map((p) => p.plan.id).join(", "),
permissions: member.permissions?.all?.join(", ") ?? "",
plans:
member.planConnections
?.map((p) => p.plan?.id)
.filter(Boolean)
.join(", ") ?? "",
...Object.fromEntries(
Object.entries(member.customFields ?? {}).map(([k, v]) => [
`customFields.${k}`,
Expand Down Expand Up @@ -205,7 +210,7 @@ membersCommand
"--after <cursor>",
"Pagination cursor (endCursor from previous page)"
)
.option("--order <order>", "Sort order (ASC or DESC)", "ASC")
.option("--order <order>", "Sort order (ASC or DESC)")
.option("--limit <number>", "Max members to return (default: 50, max: 200)")
.option("--all", "Auto-paginate and fetch all members")
.action(async (options: MembersListOptions) => {
Expand All @@ -223,29 +228,39 @@ membersCommand

const result = await graphqlRequest<{
getMembers: {
edges: { cursor: string; node: Member }[];
edges: { node: Member }[];
pageInfo: { endCursor: string | null };
};
}>({
query: `query($first: Int, $after: String, $order: OrderByInput) {
getMembers(first: $first, after: $after, order: $order) {
edges { cursor node { ${MEMBER_FIELDS} } }
query: `query($first: Int, $after: String) {
getMembers(first: $first, after: $after) {
edges { node { ${MEMBER_FIELDS} } }
pageInfo { endCursor }
}
}`,
variables: { first: perPage, after: cursor, order: options.order },
variables: { first: perPage, after: cursor },
});

const { edges } = result.getMembers;
const { edges, pageInfo } = result.getMembers;
const members = edges.map((e) => e.node);
allMembers.push(...members);

if (allMembers.length < target && edges.length === perPage) {
cursor = edges.at(-1)?.cursor;
if (
allMembers.length < target &&
edges.length === perPage &&
pageInfo.endCursor
) {
cursor = pageInfo.endCursor;
spinner.text = `Fetching members... (${allMembers.length} so far)`;
} else {
cursor = undefined;
}
} while (cursor);

if (options.order === "DESC") {
allMembers.reverse();
}

spinner.stop();

const [first] = allMembers;
Expand Down Expand Up @@ -279,11 +294,16 @@ membersCommand
const spinner = yoctoSpinner({ text: "Fetching member..." }).start();
try {
if (idOrEmail.startsWith("mem_")) {
const result = await graphqlRequest<{ currentMember: Member }>({
const result = await graphqlRequest<{
currentMember: Member | null;
}>({
query: `query($id: ID) { currentMember(id: $id) { ${MEMBER_FIELDS} } }`,
variables: { id: idOrEmail },
});
spinner.stop();
if (!result.currentMember) {
throw new Error(`Member not found: ${idOrEmail}`);
}
printRecord(result.currentMember);
} else {
const result = await graphqlRequest<{
Expand Down Expand Up @@ -418,6 +438,11 @@ membersCommand
if (member) {
printSuccess(`Member updated: ${member.id}`);
printRecord(member);
} else {
printError(
"No update options provided. Use --help to see available options."
);
process.exitCode = 1;
}
} catch (error) {
spinner.stop();
Expand Down Expand Up @@ -627,7 +652,7 @@ membersCommand
let members: Member[];

if (hasPlanFilter && !hasFieldFilter) {
const { members: fetched } = await fetchAllMembers(spinner, "ASC", {
const { members: fetched } = await fetchAllMembers(spinner, {
planIds: [options.plan],
});
members = fetched;
Expand Down Expand Up @@ -692,8 +717,10 @@ membersCommand
inactive++;
}

for (const conn of member.planConnections) {
planCounts[conn.plan.id] = (planCounts[conn.plan.id] ?? 0) + 1;
for (const conn of member.planConnections ?? []) {
if (conn.plan?.id) {
planCounts[conn.plan.id] = (planCounts[conn.plan.id] ?? 0) + 1;
}
}

const created = new Date(member.createdAt).getTime();
Expand Down
90 changes: 60 additions & 30 deletions src/commands/records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,49 @@ const extractDataFields = (
return data;
};

const fetchAllRecords = async (
spinner: ReturnType<typeof yoctoSpinner>,
tableId: string,
filter?: Record<string, unknown>
): Promise<DataRecord[]> => {
const allRecords: DataRecord[] = [];
let cursor: string | undefined;
const pageSize = 100;

do {
const result = await graphqlRequest<{
dataRecords: {
edges: { node: DataRecord }[];
pageInfo: { endCursor: string | null };
};
}>({
query: `query($tableId: ID!, $filter: DataRecordsFilterInput, $pagination: DataRecordsPaginationInput) {
dataRecords(tableId: $tableId, filter: $filter, pagination: $pagination) {
edges { node { ${DATA_RECORD_FIELDS} } }
pageInfo { endCursor }
}
}`,
variables: {
tableId,
filter,
pagination: { first: pageSize, after: cursor },
},
});

const { edges, pageInfo } = result.dataRecords;
allRecords.push(...edges.map((e) => e.node));

if (edges.length === pageSize && pageInfo.endCursor) {
cursor = pageInfo.endCursor;
spinner.text = `Fetching records... (${allRecords.length} so far)`;
} else {
cursor = undefined;
}
} while (cursor);

return allRecords;
};

const resolveTableId = async (tableKey: string): Promise<string> => {
const result = await graphqlRequest<{ dataTable: { id: string } }>({
query: "query($key: String!) { dataTable(key: $key) { id } }",
Expand Down Expand Up @@ -273,7 +316,7 @@ recordsCommand
createdAt: e.node.createdAt,
updatedAt: e.node.updatedAt,
...Object.fromEntries(
Object.entries(e.node.data).map(([k, v]) => [`data.${k}`, v])
Object.entries(e.node.data ?? {}).map(([k, v]) => [`data.${k}`, v])
),
}));
printSuccess(`Found ${records.length} record(s)`);
Expand Down Expand Up @@ -304,12 +347,14 @@ recordsCommand
const pageSize = 100;
const result = await graphqlRequest<{
dataRecords: {
edges: { cursor: string; node: DataRecord }[];
edges: { node: DataRecord }[];
pageInfo: { endCursor: string | null };
};
}>({
query: `query($tableId: ID!, $pagination: DataRecordsPaginationInput) {
dataRecords(tableId: $tableId, pagination: $pagination) {
edges { cursor node { ${DATA_RECORD_FIELDS} } }
edges { node { ${DATA_RECORD_FIELDS} } }
pageInfo { endCursor }
}
}`,
variables: {
Expand All @@ -318,26 +363,29 @@ recordsCommand
},
});

const { edges } = result.dataRecords;
const { edges, pageInfo } = result.dataRecords;

for (const { node: record } of edges) {
allRecords.push({
id: record.id,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
...Object.fromEntries(
Object.entries(record.data).map(([k, v]) => [`data.${k}`, v])
Object.entries(record.data ?? {}).map(([k, v]) => [
`data.${k}`,
v,
])
),
});
}

if (edges.length === pageSize) {
cursor = edges.at(-1)?.cursor;
if (edges.length === pageSize && pageInfo.endCursor) {
cursor = pageInfo.endCursor;
spinner.text = `Fetching records... (${allRecords.length} so far)`;
} else {
cursor = undefined;
}
} while (cursor !== undefined);
} while (cursor);

spinner.text = "Writing file...";

Expand Down Expand Up @@ -492,29 +540,11 @@ recordsCommand
const spinner = yoctoSpinner({ text: "Querying records..." }).start();
try {
const tableId = await resolveTableId(tableKey);
const variables: Record<string, unknown> = {
tableId,
pagination: { first: 100 },
};

if (options.where?.length) {
variables.filter = { fieldFilters: parseWhereClause(options.where) };
}

const result = await graphqlRequest<{
dataRecords: {
edges: { node: DataRecord }[];
};
}>({
query: `query($tableId: ID!, $filter: DataRecordsFilterInput, $pagination: DataRecordsPaginationInput) {
dataRecords(tableId: $tableId, filter: $filter, pagination: $pagination) {
edges { node { ${DATA_RECORD_FIELDS} } }
}
}`,
variables,
});
const filter = options.where?.length
? { fieldFilters: parseWhereClause(options.where) }
: undefined;

const targets = result.dataRecords.edges.map((e) => e.node);
const targets = await fetchAllRecords(spinner, tableId, filter);

if (targets.length === 0) {
spinner.stop();
Expand Down
Loading