1 |
|
2 | "use strict";
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | Object.defineProperty(exports, "__esModule", { value: true });
|
19 | exports.MachineLearningApiClient = exports.isGcsTfliteModelOptions = void 0;
|
20 | const api_request_1 = require("../utils/api-request");
|
21 | const error_1 = require("../utils/error");
|
22 | const utils = require("../utils/index");
|
23 | const validator = require("../utils/validator");
|
24 | const machine_learning_utils_1 = require("./machine-learning-utils");
|
25 | const ML_V1BETA2_API = 'https://firebaseml.googleapis.com/v1beta2';
|
26 | const FIREBASE_VERSION_HEADER = {
|
27 | 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`,
|
28 | };
|
29 |
|
30 | const POLL_DEFAULT_MAX_TIME_MILLISECONDS = 120000;
|
31 | const POLL_BASE_WAIT_TIME_MILLISECONDS = 3000;
|
32 | const POLL_MAX_WAIT_TIME_MILLISECONDS = 30000;
|
33 | function isGcsTfliteModelOptions(options) {
|
34 | const gcsUri = options?.tfliteModel?.gcsTfliteUri;
|
35 | return typeof gcsUri !== 'undefined';
|
36 | }
|
37 | exports.isGcsTfliteModelOptions = isGcsTfliteModelOptions;
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | class MachineLearningApiClient {
|
44 | constructor(app) {
|
45 | this.app = app;
|
46 | if (!validator.isNonNullObject(app) || !('options' in app)) {
|
47 | throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'First argument passed to admin.machineLearning() must be a valid '
|
48 | + 'Firebase app instance.');
|
49 | }
|
50 | this.httpClient = new api_request_1.AuthorizedHttpClient(app);
|
51 | }
|
52 | createModel(model) {
|
53 | if (!validator.isNonNullObject(model) ||
|
54 | !validator.isNonEmptyString(model.displayName)) {
|
55 | const err = new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Invalid model content.');
|
56 | return Promise.reject(err);
|
57 | }
|
58 | return this.getProjectUrl()
|
59 | .then((url) => {
|
60 | const request = {
|
61 | method: 'POST',
|
62 | url: `${url}/models`,
|
63 | data: model,
|
64 | };
|
65 | return this.sendRequest(request);
|
66 | });
|
67 | }
|
68 | updateModel(modelId, model, updateMask) {
|
69 | if (!validator.isNonEmptyString(modelId) ||
|
70 | !validator.isNonNullObject(model) ||
|
71 | !validator.isNonEmptyArray(updateMask)) {
|
72 | const err = new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Invalid model or mask content.');
|
73 | return Promise.reject(err);
|
74 | }
|
75 | return this.getProjectUrl()
|
76 | .then((url) => {
|
77 | const request = {
|
78 | method: 'PATCH',
|
79 | url: `${url}/models/${modelId}?updateMask=${updateMask.join()}`,
|
80 | data: model,
|
81 | };
|
82 | return this.sendRequest(request);
|
83 | });
|
84 | }
|
85 | getModel(modelId) {
|
86 | return Promise.resolve()
|
87 | .then(() => {
|
88 | return this.getModelName(modelId);
|
89 | })
|
90 | .then((modelName) => {
|
91 | return this.getResourceWithShortName(modelName);
|
92 | });
|
93 | }
|
94 | getOperation(operationName) {
|
95 | return Promise.resolve()
|
96 | .then(() => {
|
97 | return this.getResourceWithFullName(operationName);
|
98 | });
|
99 | }
|
100 | listModels(options = {}) {
|
101 | if (!validator.isNonNullObject(options)) {
|
102 | const err = new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Invalid ListModelsOptions');
|
103 | return Promise.reject(err);
|
104 | }
|
105 | if (typeof options.filter !== 'undefined' && !validator.isNonEmptyString(options.filter)) {
|
106 | const err = new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Invalid list filter.');
|
107 | return Promise.reject(err);
|
108 | }
|
109 | if (typeof options.pageSize !== 'undefined') {
|
110 | if (!validator.isNumber(options.pageSize)) {
|
111 | const err = new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Invalid page size.');
|
112 | return Promise.reject(err);
|
113 | }
|
114 | if (options.pageSize < 1 || options.pageSize > 100) {
|
115 | const err = new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Page size must be between 1 and 100.');
|
116 | return Promise.reject(err);
|
117 | }
|
118 | }
|
119 | if (typeof options.pageToken !== 'undefined' && !validator.isNonEmptyString(options.pageToken)) {
|
120 | const err = new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Next page token must be a non-empty string.');
|
121 | return Promise.reject(err);
|
122 | }
|
123 | return this.getProjectUrl()
|
124 | .then((url) => {
|
125 | const request = {
|
126 | method: 'GET',
|
127 | url: `${url}/models`,
|
128 | data: options,
|
129 | };
|
130 | return this.sendRequest(request);
|
131 | });
|
132 | }
|
133 | deleteModel(modelId) {
|
134 | return this.getProjectUrl()
|
135 | .then((url) => {
|
136 | const modelName = this.getModelName(modelId);
|
137 | const request = {
|
138 | method: 'DELETE',
|
139 | url: `${url}/${modelName}`,
|
140 | };
|
141 | return this.sendRequest(request);
|
142 | });
|
143 | }
|
144 | |
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 | handleOperation(op, options) {
|
151 | if (op.done) {
|
152 | if (op.response) {
|
153 | return Promise.resolve(op.response);
|
154 | }
|
155 | else if (op.error) {
|
156 | const err = machine_learning_utils_1.FirebaseMachineLearningError.fromOperationError(op.error.code, op.error.message);
|
157 | return Promise.reject(err);
|
158 | }
|
159 |
|
160 | throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-server-response', 'Invalid operation response.');
|
161 | }
|
162 |
|
163 | if (options?.wait) {
|
164 | return this.pollOperationWithExponentialBackoff(op.name, options);
|
165 | }
|
166 | const metadata = op.metadata || {};
|
167 | const metadataType = metadata['@type'] || '';
|
168 | if (!metadataType.includes('ModelOperationMetadata')) {
|
169 | throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-server-response', `Unknown Metadata type: ${JSON.stringify(metadata)}`);
|
170 | }
|
171 | return this.getModel(extractModelId(metadata.name));
|
172 | }
|
173 |
|
174 | pollOperationWithExponentialBackoff(opName, options) {
|
175 | const maxTimeMilliseconds = options?.maxTimeMillis ?? POLL_DEFAULT_MAX_TIME_MILLISECONDS;
|
176 | const baseWaitMillis = options?.baseWaitMillis ?? POLL_BASE_WAIT_TIME_MILLISECONDS;
|
177 | const maxWaitMillis = options?.maxWaitMillis ?? POLL_MAX_WAIT_TIME_MILLISECONDS;
|
178 | const poller = new api_request_1.ExponentialBackoffPoller(baseWaitMillis, maxWaitMillis, maxTimeMilliseconds);
|
179 | return poller.poll(() => {
|
180 | return this.getOperation(opName)
|
181 | .then((responseData) => {
|
182 | if (!responseData.done) {
|
183 | return null;
|
184 | }
|
185 | if (responseData.error) {
|
186 | const err = machine_learning_utils_1.FirebaseMachineLearningError.fromOperationError(responseData.error.code, responseData.error.message);
|
187 | throw err;
|
188 | }
|
189 | return responseData.response;
|
190 | });
|
191 | });
|
192 | }
|
193 | |
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | getResourceWithShortName(name) {
|
201 | return this.getProjectUrl()
|
202 | .then((url) => {
|
203 | const request = {
|
204 | method: 'GET',
|
205 | url: `${url}/${name}`,
|
206 | };
|
207 | return this.sendRequest(request);
|
208 | });
|
209 | }
|
210 | |
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 | getResourceWithFullName(fullName) {
|
217 | const request = {
|
218 | method: 'GET',
|
219 | url: `${ML_V1BETA2_API}/${fullName}`
|
220 | };
|
221 | return this.sendRequest(request);
|
222 | }
|
223 | sendRequest(request) {
|
224 | request.headers = FIREBASE_VERSION_HEADER;
|
225 | return this.httpClient.send(request)
|
226 | .then((resp) => {
|
227 | return resp.data;
|
228 | })
|
229 | .catch((err) => {
|
230 | throw this.toFirebaseError(err);
|
231 | });
|
232 | }
|
233 | toFirebaseError(err) {
|
234 | if (err instanceof error_1.PrefixedFirebaseError) {
|
235 | return err;
|
236 | }
|
237 | const response = err.response;
|
238 | if (!response.isJson()) {
|
239 | return new machine_learning_utils_1.FirebaseMachineLearningError('unknown-error', `Unexpected response with status: ${response.status} and body: ${response.text}`);
|
240 | }
|
241 | const error = response.data.error || {};
|
242 | let code = 'unknown-error';
|
243 | if (error.status && error.status in ERROR_CODE_MAPPING) {
|
244 | code = ERROR_CODE_MAPPING[error.status];
|
245 | }
|
246 | const message = error.message || `Unknown server error: ${response.text}`;
|
247 | return new machine_learning_utils_1.FirebaseMachineLearningError(code, message);
|
248 | }
|
249 | getProjectUrl() {
|
250 | return this.getProjectIdPrefix()
|
251 | .then((projectIdPrefix) => {
|
252 | return `${ML_V1BETA2_API}/${projectIdPrefix}`;
|
253 | });
|
254 | }
|
255 | getProjectIdPrefix() {
|
256 | if (this.projectIdPrefix) {
|
257 | return Promise.resolve(this.projectIdPrefix);
|
258 | }
|
259 | return utils.findProjectId(this.app)
|
260 | .then((projectId) => {
|
261 | if (!validator.isNonEmptyString(projectId)) {
|
262 | throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Failed to determine project ID. Initialize the SDK with service account credentials, or '
|
263 | + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT '
|
264 | + 'environment variable.');
|
265 | }
|
266 | this.projectIdPrefix = `projects/${projectId}`;
|
267 | return this.projectIdPrefix;
|
268 | });
|
269 | }
|
270 | getModelName(modelId) {
|
271 | if (!validator.isNonEmptyString(modelId)) {
|
272 | throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Model ID must be a non-empty string.');
|
273 | }
|
274 | if (modelId.indexOf('/') !== -1) {
|
275 | throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', 'Model ID must not contain any "/" characters.');
|
276 | }
|
277 | return `models/${modelId}`;
|
278 | }
|
279 | }
|
280 | exports.MachineLearningApiClient = MachineLearningApiClient;
|
281 | const ERROR_CODE_MAPPING = {
|
282 | INVALID_ARGUMENT: 'invalid-argument',
|
283 | NOT_FOUND: 'not-found',
|
284 | RESOURCE_EXHAUSTED: 'resource-exhausted',
|
285 | UNAUTHENTICATED: 'authentication-error',
|
286 | UNKNOWN: 'unknown-error',
|
287 | };
|
288 | function extractModelId(resourceName) {
|
289 | return resourceName.split('/').pop();
|
290 | }
|