Skip to content

Commit 70f8218

Browse files
Account Management API: Added limit/skip fields to support pagination when querying for licenses managed by a particular user.
1 parent ca186f0 commit 70f8218

File tree

6 files changed

+261
-74
lines changed

6 files changed

+261
-74
lines changed

src/packages/next/lib/api/schema/common.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,17 @@ export const RequestNoCacheSchema = z
2929
.optional();
3030

3131
export type RequestNoCache = z.infer<typeof RequestNoCacheSchema>;
32+
33+
export const LimitResultsSchema = z
34+
.number()
35+
.min(1)
36+
.describe("Limits the number of returned results.");
37+
38+
export type LimitResults = z.infer<typeof LimitResultsSchema>;
39+
40+
export const SkipResultsSchema = z
41+
.number()
42+
.min(1)
43+
.describe("Skips the first `n` results.");
44+
45+
export type SkipResults = z.infer<typeof SkipResultsSchema>;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* See src/packages/util/db-schema/site-licenses.ts for the database schema corresponding
2+
to this type.
3+
*/
4+
5+
import { z } from "../../framework";
6+
7+
export const SiteLicenseIdSchema = z
8+
.string()
9+
.uuid()
10+
.describe("Site license id.");
11+
12+
export const SiteLicenseIdleTimeoutSchema = z
13+
.enum(["short", "medium", "day"])
14+
.describe("Available options for finite project idle run times.");
15+
16+
export const SiteLicenseUptimeSchema = z
17+
.union([SiteLicenseIdleTimeoutSchema, z.literal("always_running")])
18+
.describe(
19+
`Determines how long a project runs while not being used before being automatically
20+
stopped. A \`short\` value corresponds to a 30-minute timeout, and a \`medium\` value
21+
to a 2-hour timeout.`,
22+
);
23+
24+
export const SiteLicenseRunLimitSchema = z
25+
.number()
26+
.min(0)
27+
.describe("Number of projects which may simultaneously use this license");
28+
29+
export const SiteLicenseQuotaSchema = z.object({
30+
always_running: z
31+
.boolean()
32+
.nullish()
33+
.describe(
34+
`Indicates whether the project(s) this license is applied to should be
35+
allowed to always be running.`,
36+
),
37+
boost: z
38+
.boolean()
39+
.nullish()
40+
.describe(
41+
`If \`true\`, this license is a boost license and allows for a project to
42+
temporarily boost the amount of resources available to a project by the amount
43+
specified in the \`cpu\`, \`memory\`, and \`disk\` fields.`,
44+
),
45+
cpu: z
46+
.number()
47+
.min(1)
48+
.describe("Limits the total number of vCPUs allocated to a project."),
49+
dedicated_cpu: z.number().min(1),
50+
dedicated_ram: z.number().min(1),
51+
disk: z
52+
.number()
53+
.min(1)
54+
.describe(
55+
`Disk size in GB to be allocated to the project to which this license is
56+
applied.`,
57+
),
58+
idle_timeout: SiteLicenseIdleTimeoutSchema,
59+
member: z.boolean().describe(
60+
`Member hosting significantly reduces competition for resources, and we
61+
prioritize support requests much higher. _Please be aware: licenses of
62+
different member hosting service levels cannot be combined!_`,
63+
),
64+
ram: z
65+
.number()
66+
.min(1)
67+
.describe(
68+
"Limits the total memory a project can use. At least 2GB is recommended.",
69+
),
70+
user: z.enum(["academic", "business"]).describe("User type."),
71+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { z } from "../../framework";
2+
3+
import { FailedAPIOperationSchema } from "../common";
4+
import { AccountIdSchema } from "../accounts/common";
5+
6+
import {
7+
SiteLicenseIdSchema,
8+
SiteLicenseQuotaSchema,
9+
SiteLicenseRunLimitSchema,
10+
} from "./common";
11+
12+
// OpenAPI spec
13+
//
14+
export const GetManagedLicensesInputSchema = z
15+
.object({
16+
limit: z
17+
.number()
18+
.min(1)
19+
.optional()
20+
.describe("Limits number of results to the provided value."),
21+
skip: z.number().min(1).optional().describe("Skips the first `n` results."),
22+
})
23+
.describe(
24+
`Fetch all licenses which are managed by a particular \`account_id\`. Results are
25+
returned in descending order of license creation time.`,
26+
);
27+
28+
export const GetManagedLicensesOutputSchema = z.union([
29+
FailedAPIOperationSchema,
30+
z
31+
.object({
32+
id: SiteLicenseIdSchema,
33+
title: z.string().optional().describe("User-defined license title."),
34+
description: z
35+
.string()
36+
.optional()
37+
.describe("User-defined license description."),
38+
expires: z
39+
.number()
40+
.optional()
41+
.describe(
42+
"UNIX timestamp (in milliseconds) which indicates when the license is to expire.",
43+
),
44+
activates: z.number().describe(
45+
`UNIX timestamp (in milliseconds) which indicates when the license takes effect.
46+
Licenses may be applied prior to this time, but will not take effect before it.`,
47+
),
48+
last_used: z
49+
.number()
50+
.optional()
51+
.describe(
52+
`UNIX timestamp (in milliseconds) which indicates when the license was last used
53+
to upgrade a project while it started.`,
54+
),
55+
created: z
56+
.number()
57+
.optional()
58+
.describe(
59+
"UNIX timestamp (in milliseconds) which indicates when the license was created.",
60+
),
61+
managers: z
62+
.array(AccountIdSchema)
63+
.describe(
64+
"A list of account ids which are permitted to manage this license.",
65+
),
66+
upgrades: z
67+
.object({
68+
cores: z.number().min(0),
69+
cpu_shares: z.number().min(0),
70+
disk_quota: z.number().min(0),
71+
memory: z.number().min(0),
72+
mintime: z.number().min(0),
73+
network: z.number().min(0),
74+
})
75+
.optional()
76+
.describe(
77+
`**Deprecated:** A map of upgrades that are applied to a project when it has
78+
this site license. This is been deprecated in favor of the \`quota\` field.`,
79+
),
80+
quota: SiteLicenseQuotaSchema.optional().describe(
81+
"The exact resource quota allotted to this license.",
82+
),
83+
run_limit: SiteLicenseRunLimitSchema,
84+
info: z
85+
.record(z.string(), z.any())
86+
.optional()
87+
.describe(
88+
`Structured object in which admins may store additional license information.
89+
This field generally contains purchase information for the license (e.g.,
90+
purchasing account, etc.)`,
91+
),
92+
})
93+
.describe(
94+
`Defines a site license object, which is used to determine resource limits (e.g.,
95+
CPU, memory, disk space, etc.) to be applied to a running project.`,
96+
),
97+
]);
98+
99+
export type GetManagedLicensesInput = z.infer<
100+
typeof GetManagedLicensesInputSchema
101+
>;
102+
export type GetManagedLicensesOutput = z.infer<
103+
typeof GetManagedLicensesOutputSchema
104+
>;

src/packages/next/lib/api/schema/shopping/cart/add.ts

Lines changed: 16 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { z } from "../../../framework";
33
import { FailedAPIOperationSchema, OkAPIOperationSchema } from "../../common";
44

55
import { ProjectIdSchema } from "../../projects/common";
6+
import {
7+
SiteLicenseQuotaSchema,
8+
SiteLicenseRunLimitSchema,
9+
SiteLicenseUptimeSchema,
10+
} from "../../licenses/common";
611

712
const LicenseRangeSchema = z
813
.array(z.string())
@@ -48,71 +53,18 @@ export const ShoppingCartAddInputSchema = z
4853
.nullish(),
4954
description: z
5055
.union([
51-
z
52-
.object({
53-
title: LicenseTitleSchema.optional(),
54-
description: LicenseDescriptionSchema.optional(),
55-
range: LicenseRangeSchema.optional(),
56-
period: z.enum(["range", "monthly", "yearly"]).describe(
57-
`Period for which this license is to be applied. If \`range\` is selected,
56+
SiteLicenseQuotaSchema.extend({
57+
title: LicenseTitleSchema.optional(),
58+
description: LicenseDescriptionSchema.optional(),
59+
range: LicenseRangeSchema.optional(),
60+
period: z.enum(["range", "monthly", "yearly"]).describe(
61+
`Period for which this license is to be applied. If \`range\` is selected,
5862
the \`range\` field must be populated in this request.`,
59-
),
60-
type: z.enum(["quota"]).describe("License type"),
61-
user: z.enum(["academic", "business"]).describe("User type."),
62-
run_limit: z
63-
.number()
64-
.min(0)
65-
.describe(
66-
"Number of projects which may simultaneously use this license",
67-
),
68-
always_running: z
69-
.boolean()
70-
.nullish()
71-
.describe(
72-
`Indicates whether the project(s) this license is applied to should be
73-
allowed to always be running.`,
74-
),
75-
ram: z
76-
.number()
77-
.min(1)
78-
.describe(
79-
"Limits the total memory a project can use. At least 2GB is recommended.",
80-
),
81-
cpu: z
82-
.number()
83-
.min(1)
84-
.describe(
85-
"Limits the total number of vCPUs allocated to a project.",
86-
),
87-
disk: z
88-
.number()
89-
.min(1)
90-
.describe(
91-
`Disk size in GB to be allocated to the project to which this license is
92-
applied.`,
93-
),
94-
member: z.boolean().describe(
95-
`Member hosting significantly reduces competition for resources, and we
96-
prioritize support requests much higher. _Please be aware: licenses of
97-
different member hosting service levels cannot be combined!_`,
98-
),
99-
uptime: z
100-
.enum(["short", "medium", "day", "always_running"])
101-
.describe(
102-
`Determines how long a project runs while not being used before being
103-
automatically stopped. A \`short\` value corresponds to a 30-minute
104-
timeout, and a \`medium\` value to a 2-hour timeout.`,
105-
),
106-
boost: z
107-
.boolean()
108-
.nullish()
109-
.describe(
110-
`If \`true\`, this license is a boost license and allows for a project to
111-
temporarily boost the amount of resources available to a project by the
112-
amount specified in the \`cpu\`, \`memory\`, and \`disk\` fields.`,
113-
),
114-
})
115-
.describe("Project resource quote license."),
63+
),
64+
type: z.enum(["quota"]).describe("License type"),
65+
run_limit: SiteLicenseRunLimitSchema,
66+
uptime: SiteLicenseUptimeSchema,
67+
}).describe("Project resource quota license."),
11668
z
11769
.object({
11870
title: LicenseTitleSchema,

src/packages/next/pages/api/v2/licenses/get-managed.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@ import getManagedLicenses, {
1111
License,
1212
} from "@cocalc/server/licenses/get-managed";
1313
import getAccountId from "lib/account/get-account";
14+
import getParams from "lib/api/get-params";
1415

15-
export default async function handle(req, res) {
16+
import { apiRoute, apiRouteOperation } from "lib/api";
17+
import {
18+
GetManagedLicensesInputSchema,
19+
GetManagedLicensesOutputSchema,
20+
} from "lib/api/schema/licenses/get-managed";
21+
22+
async function handle(req, res) {
1623
try {
1724
res.json(await get(req));
1825
} catch (err) {
@@ -26,5 +33,27 @@ async function get(req): Promise<License[]> {
2633
if (account_id == null) {
2734
return [];
2835
}
29-
return await getManagedLicenses(account_id);
36+
const { limit, skip } = getParams(req);
37+
return await getManagedLicenses(account_id, limit, skip);
3038
}
39+
40+
export default apiRoute({
41+
getManaged: apiRouteOperation({
42+
method: "POST",
43+
openApiOperation: {
44+
tags: ["Licenses"],
45+
},
46+
})
47+
.input({
48+
contentType: "application/json",
49+
body: GetManagedLicensesInputSchema,
50+
})
51+
.outputs([
52+
{
53+
status: 200,
54+
contentType: "application/json",
55+
body: GetManagedLicensesOutputSchema,
56+
},
57+
])
58+
.handler(handle),
59+
});

src/packages/server/licenses/get-managed.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,38 @@ import type { License } from "@cocalc/util/db-schema/site-licenses";
99
export type { License };
1010

1111
export default async function getManagedLicenses(
12-
account_id: string
12+
account_id: string,
13+
limit?: number,
14+
offset?: number,
1315
): Promise<License[]> {
1416
if (!isValidUUID(account_id)) {
1517
throw Error("invalid account_id -- must be a uuid");
1618
}
1719

1820
const pool = getPool();
21+
const params = [account_id];
1922

20-
const { rows } = await pool.query(
21-
`SELECT id, title, description,
22-
expires, activates, last_used, created,
23-
managers, upgrades, quota, run_limit, info
24-
FROM site_licenses WHERE $1=ANY(managers) ORDER BY created DESC`,
25-
[account_id]
26-
);
23+
let query = `
24+
SELECT id, title, description, expires, activates, last_used, created, managers,
25+
upgrades, quota, run_limit, info
26+
FROM site_licenses WHERE $1=ANY(managers) ORDER BY created DESC
27+
`;
28+
29+
// (Optional) pagination
30+
//
31+
if (limit) {
32+
query += ` LIMIT $${params.length + 1}`;
33+
params.push(limit.toString());
34+
}
35+
36+
if (offset) {
37+
query += ` OFFSET $${params.length + 1}`;
38+
params.push(offset.toString());
39+
}
40+
41+
// Execute query
42+
//
43+
const { rows } = await pool.query(query, params);
2744
toEpoch(rows, ["expires", "activates", "last_used", "created"]);
2845
for (const row of rows) {
2946
row.is_manager = true;

0 commit comments

Comments
 (0)